feat: Implementar todas correções desde desvinculação Netlify

- Corrigir sintaxe PostgREST em todos serviços (getById, update, delete)
  * patientService: ?id=eq.uuid
  * doctorService: ?id=eq.uuid
  * appointmentService: ?id=eq.uuid
  * reportService: ?id=eq.uuid

- Adicionar componente Chatbot com IA
  * Respostas inteligentes baseadas em palavras-chave
  * Quick replies para perguntas frequentes
  * Integrado em CentralAjuda e CentralAjudaMedico

- Padronizar LoginPaciente
  * Usar loginComEmailSenha (mesma API de LoginSecretaria)
  * Remover lógica antiga de loginPaciente
  * Simplificar fluxo de autenticação
This commit is contained in:
guisilvagomes 2025-10-23 17:21:21 -03:00
parent e163a1dc7e
commit 0d3cad1b55
34 changed files with 275 additions and 3884 deletions

View File

@ -1,163 +0,0 @@
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return { statusCode: 200, headers, body: "" };
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token n<>o fornecido" }),
};
}
const pathParts = event.path.split("/");
const appointmentId =
pathParts[pathParts.length - 1] !== "appointments"
? pathParts[pathParts.length - 1]
: null;
if (event.httpMethod === "GET") {
let url = `${SUPABASE_URL}/rest/v1/appointments`;
if (appointmentId && appointmentId !== "appointments") {
url += `?id=eq.${appointmentId}&select=*`;
} else if (event.queryStringParameters) {
const params = new URLSearchParams(
event.queryStringParameters as Record<string, string>
);
url += `?${params.toString()}`;
if (!params.has("select")) {
url += url.includes("?") ? "&select=*" : "?select=*";
}
} else {
url += "?select=*";
}
const response = await fetch(url, {
method: "GET",
headers: { apikey: SUPABASE_ANON_KEY, Authorization: authHeader },
});
let data = await response.json();
if (
appointmentId &&
appointmentId !== "appointments" &&
Array.isArray(data) &&
data.length > 0
) {
data = data[0];
}
return {
statusCode: response.status,
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(data),
};
}
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
if (!body.patient_id || !body.doctor_id || !body.scheduled_at) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigat<61>rios: patient_id, doctor_id, scheduled_at",
}),
};
}
const response = await fetch(`${SUPABASE_URL}/rest/v1/appointments`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
});
let data = await response.json();
if (Array.isArray(data) && data.length > 0) data = data[0];
return {
statusCode: response.status,
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(data),
};
}
if (event.httpMethod === "PATCH") {
if (!appointmentId || appointmentId === "appointments") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do agendamento <20> obrigat<61>rio" }),
};
}
const body = JSON.parse(event.body || "{}");
const response = await fetch(
`${SUPABASE_URL}/rest/v1/appointments?id=eq.${appointmentId}`,
{
method: "PATCH",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
}
);
let data = await response.json();
if (Array.isArray(data) && data.length > 0) data = data[0];
return {
statusCode: response.status,
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(data),
};
}
if (event.httpMethod === "DELETE") {
if (!appointmentId || appointmentId === "appointments") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do agendamento <20> obrigat<61>rio" }),
};
}
const response = await fetch(
`${SUPABASE_URL}/rest/v1/appointments?id=eq.${appointmentId}`,
{
method: "DELETE",
headers: { apikey: SUPABASE_ANON_KEY, Authorization: authHeader },
}
);
return { statusCode: response.status, headers, body: "" };
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({ error: "Erro interno" }),
};
}
};

View File

@ -1,153 +0,0 @@
/**
* Netlify Function: Listar Atribuições
* GET /rest/v1/patient_assignments
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
// GET - Listar atribuições
if (event.httpMethod === "GET") {
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Monta URL com query params (se houver)
const queryString = event.queryStringParameters
? "?" +
new URLSearchParams(
event.queryStringParameters as Record<string, string>
).toString()
: "";
const response = await fetch(
`${SUPABASE_URL}/rest/v1/patient_assignments${queryString}`,
{
method: "GET",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
}
);
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro ao listar atribuições:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
}
// POST - Criar atribuição
if (event.httpMethod === "POST") {
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
const body = JSON.parse(event.body || "{}");
if (!body.patient_id || !body.user_id || !body.role) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "patient_id, user_id e role são obrigatórios",
}),
};
}
const response = await fetch(
`${SUPABASE_URL}/rest/v1/patient_assignments`,
{
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
}
);
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro ao criar atribuição:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
};

View File

@ -1,95 +0,0 @@
/**
* Netlify Function: Login
* Faz proxy seguro para API Supabase com apikey protegida
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
// Constantes da API (protegidas no backend)
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
interface LoginRequest {
email: string;
password: string;
}
export const handler: Handler = async (event: HandlerEvent) => {
// CORS headers
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
// Handle preflight
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
// Apenas POST é permitido
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
// Parse body
const body: LoginRequest = JSON.parse(event.body || "{}");
if (!body.email || !body.password) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "Email e senha são obrigatórios" }),
};
}
// Faz requisição para API Supabase COM a apikey protegida
const response = await fetch(
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
apikey: SUPABASE_ANON_KEY,
},
body: JSON.stringify({
email: body.email,
password: body.password,
}),
}
);
const data = await response.json();
// Repassa a resposta para o frontend
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro no login:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,90 +0,0 @@
/**
* Netlify Function: Logout
* Invalida a sessão do usuário no Supabase
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
// Pega o Bearer token do header
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Faz logout no Supabase
const response = await fetch(`${SUPABASE_URL}/auth/v1/logout`, {
method: "POST",
headers: {
"Content-Type": "application/json",
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
// Logout retorna 204 No Content (sem body)
if (response.status === 204) {
return {
statusCode: 204,
headers,
body: "",
};
}
// Se não for 204, retorna o body da resposta
const data = await response.text();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: data || "{}",
};
} catch (error) {
console.error("Erro no logout:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,95 +0,0 @@
/**
* Netlify Function: Magic Link
* Envia link de autenticação sem senha por email
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
// Constantes da API (protegidas no backend)
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
interface MagicLinkRequest {
email: string;
redirect_url?: string;
}
export const handler: Handler = async (event: HandlerEvent) => {
// CORS headers
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
// Handle preflight
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
// Apenas POST é permitido
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
// Parse body
const body: MagicLinkRequest = JSON.parse(event.body || "{}");
if (!body.email) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "Email é obrigatório" }),
};
}
// Faz requisição para API Supabase COM a apikey protegida
const response = await fetch(`${SUPABASE_URL}/auth/v1/otp`, {
method: "POST",
headers: {
"Content-Type": "application/json",
apikey: SUPABASE_ANON_KEY,
},
body: JSON.stringify({
email: body.email,
options: {
emailRedirectTo:
body.redirect_url ||
"https://mediconnectbrasil.netlify.app/auth/callback",
},
}),
});
const data = await response.json();
// Repassa a resposta para o frontend
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("[auth-magic-link] Erro:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro ao enviar magic link",
details: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,87 +0,0 @@
/**
* Netlify Function: Refresh Token
* Renova o access token usando o refresh token
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
interface RefreshTokenRequest {
refresh_token: string;
}
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const body: RefreshTokenRequest = JSON.parse(event.body || "{}");
if (!body.refresh_token) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "Refresh token é obrigatório" }),
};
}
// Faz requisição para renovar token no Supabase
const response = await fetch(
`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
apikey: SUPABASE_ANON_KEY,
},
body: JSON.stringify({
refresh_token: body.refresh_token,
}),
}
);
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro ao renovar token:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,77 +0,0 @@
/**
* Netlify Function: Auth User
* GET /auth/v1/user - Retorna dados do usuário autenticado
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
if (event.httpMethod === "GET") {
const response = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
method: "GET",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de auth user:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,94 +0,0 @@
/**
* Netlify Function: Delete Avatar
* DELETE /storage/v1/object/avatars/{userId}/avatar
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "DELETE, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "DELETE") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
const userId = event.queryStringParameters?.userId;
if (!userId) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "userId é obrigatório" }),
};
}
const response = await fetch(
`${SUPABASE_URL}/storage/v1/object/avatars/${userId}/avatar`,
{
method: "DELETE",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
}
);
// DELETE pode retornar 200 com body vazio
const contentType = response.headers.get("content-type");
const data = contentType?.includes("application/json")
? await response.json()
: {};
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro ao deletar avatar:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,145 +0,0 @@
/**
* Netlify Function: Upload Avatar
* POST /storage/v1/object/avatars/{userId}/avatar
*
* Aceita JSON com base64 para simplificar o upload via Netlify Functions
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Extrai userId do query string
const userId = event.queryStringParameters?.userId;
if (!userId) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "userId é obrigatório" }),
};
}
// Parse JSON body com base64
let fileData: string;
let contentType: string;
try {
const body = JSON.parse(event.body || "{}");
fileData = body.fileData; // base64 string
contentType = body.contentType || "image/jpeg";
if (!fileData) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "fileData (base64) é obrigatório" }),
};
}
} catch {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Body deve ser JSON válido com fileData em base64",
}),
};
}
// Converte base64 para Buffer
const buffer = Buffer.from(fileData, "base64");
// Upload para Supabase Storage
const response = await fetch(
`${SUPABASE_URL}/storage/v1/object/avatars/${userId}/avatar`,
{
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": contentType,
"x-upsert": "true", // Sobrescreve se já existir
},
body: buffer,
}
);
const data = await response.json();
if (!response.ok) {
console.error("Erro do Supabase:", data);
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
error: data.error || "Erro ao fazer upload no Supabase",
details: data,
}),
};
}
return {
statusCode: 200,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
message: "Upload realizado com sucesso",
path: data.Key || data.path,
fullPath: data.Key || data.path,
}),
};
} catch (error) {
console.error("Erro no upload do avatar:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,163 +0,0 @@
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return { statusCode: 200, headers, body: "" };
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token n<>o fornecido" }),
};
}
const pathParts = event.path.split("/");
const appointmentId =
pathParts[pathParts.length - 1] !== "consultas"
? pathParts[pathParts.length - 1]
: null;
if (event.httpMethod === "GET") {
let url = `${SUPABASE_URL}/rest/v1/appointments`;
if (appointmentId && appointmentId !== "consultas") {
url += `?id=eq.${appointmentId}&select=*`;
} else if (event.queryStringParameters) {
const params = new URLSearchParams(
event.queryStringParameters as Record<string, string>
);
url += `?${params.toString()}`;
if (!params.has("select")) {
url += url.includes("?") ? "&select=*" : "?select=*";
}
} else {
url += "?select=*";
}
const response = await fetch(url, {
method: "GET",
headers: { apikey: SUPABASE_ANON_KEY, Authorization: authHeader },
});
let data = await response.json();
if (
appointmentId &&
appointmentId !== "consultas" &&
Array.isArray(data) &&
data.length > 0
) {
data = data[0];
}
return {
statusCode: response.status,
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(data),
};
}
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
if (!body.patient_id || !body.doctor_id || !body.scheduled_at) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigat<61>rios: patient_id, doctor_id, scheduled_at",
}),
};
}
const response = await fetch(`${SUPABASE_URL}/rest/v1/appointments`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
});
let data = await response.json();
if (Array.isArray(data) && data.length > 0) data = data[0];
return {
statusCode: response.status,
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(data),
};
}
if (event.httpMethod === "PATCH") {
if (!appointmentId || appointmentId === "consultas") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do agendamento <20> obrigat<61>rio" }),
};
}
const body = JSON.parse(event.body || "{}");
const response = await fetch(
`${SUPABASE_URL}/rest/v1/appointments?id=eq.${appointmentId}`,
{
method: "PATCH",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
}
);
let data = await response.json();
if (Array.isArray(data) && data.length > 0) data = data[0];
return {
statusCode: response.status,
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(data),
};
}
if (event.httpMethod === "DELETE") {
if (!appointmentId || appointmentId === "consultas") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do agendamento <20> obrigat<61>rio" }),
};
}
const response = await fetch(
`${SUPABASE_URL}/rest/v1/appointments?id=eq.${appointmentId}`,
{
method: "DELETE",
headers: { apikey: SUPABASE_ANON_KEY, Authorization: authHeader },
}
);
return { statusCode: response.status, headers, body: "" };
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({ error: "Erro interno" }),
};
}
};

View File

@ -1,100 +0,0 @@
/**
* Netlify Function: Create Doctor
* POST /create-doctor - Cria registro de médico com validações
* Não cria auth user - apenas registro na tabela doctors
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
const body = JSON.parse(event.body || "{}");
// Validação dos campos obrigatórios
if (
!body.email ||
!body.full_name ||
!body.cpf ||
!body.crm ||
!body.crm_uf
) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: email, full_name, cpf, crm, crm_uf",
}),
};
}
// Chama a Edge Function do Supabase para criar médico
const response = await fetch(`${SUPABASE_URL}/functions/v1/create-doctor`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro na API de create doctor:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,102 +0,0 @@
/**
* Netlify Function: Create Patient
* POST /create-patient - Cria registro de paciente diretamente
* Não cria auth user - apenas registro na tabela patients
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
const body = JSON.parse(event.body || "{}");
// Validação dos campos obrigatórios
if (
!body.full_name ||
!body.cpf ||
!body.email ||
!body.phone_mobile ||
!body.created_by
) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error:
"Campos obrigatórios: full_name, cpf, email, phone_mobile, created_by",
}),
};
}
// Chama REST API do Supabase para criar paciente diretamente
const response = await fetch(`${SUPABASE_URL}/rest/v1/patients`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
});
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro na API de create patient:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,223 +0,0 @@
/**
* Netlify Function: Create User With Password
* POST /create-user-with-password - Cria usuário com senha
* Usa Edge Function do Supabase (não Admin API)
* Requer permissão de admin, gestor ou secretaria
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization, apikey",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
console.error("[create-user-with-password] Token não fornecido!");
return {
statusCode: 401,
headers,
body: JSON.stringify({
error: "Token de autenticação é obrigatório",
}),
};
}
const body = JSON.parse(event.body || "{}");
console.log(
"[create-user-with-password] Recebido:",
JSON.stringify({ ...body, password: "***" }, null, 2)
);
// Validações
if (!body.email || !body.password || !body.full_name) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: email, password, full_name",
}),
};
}
if (body.password.length < 6) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Senha deve ter no mínimo 6 caracteres",
}),
};
}
// 1. Criar usuário via Edge Function do Supabase
console.log(
"[create-user-with-password] Chamando Edge Function do Supabase..."
);
console.log(
"[create-user-with-password] URL:",
`${SUPABASE_URL}/functions/v1/create-user`
);
console.log("[create-user-with-password] Payload:", {
email: body.email,
has_password: !!body.password,
full_name: body.full_name,
});
const createUserResponse = await fetch(
`${SUPABASE_URL}/functions/v1/create-user`,
{
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
},
body: JSON.stringify({
email: body.email,
password: body.password,
full_name: body.full_name,
phone: body.phone || null,
role: body.role || "user",
}),
}
);
console.log(
"[create-user-with-password] Status da resposta:",
createUserResponse.status
);
console.log(
"[create-user-with-password] Status text:",
createUserResponse.statusText
);
// Sempre tenta ler a resposta como JSON
let responseData;
try {
responseData = await createUserResponse.json();
console.log(
"[create-user-with-password] Resposta JSON:",
JSON.stringify(responseData, null, 2)
);
} catch (error) {
const responseText = await createUserResponse.text();
console.error(
"[create-user-with-password] Resposta não é JSON:",
responseText
);
console.error("[create-user-with-password] Erro ao parsear JSON:", error);
return {
statusCode: 500,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
error: "Erro ao processar resposta do Supabase",
details: responseText,
}),
};
}
if (!createUserResponse.ok) {
console.error(
"[create-user-with-password] Erro ao criar usuário:",
JSON.stringify(responseData, null, 2)
);
return {
statusCode: createUserResponse.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
error:
responseData.msg || responseData.message || "Erro ao criar usuário",
details: responseData,
}),
};
}
// Verificar se a Edge Function retornou sucesso
if (!responseData.success) {
console.error(
"[create-user-with-password] Edge Function retornou erro:",
JSON.stringify(responseData, null, 2)
);
return {
statusCode: 400,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
error: responseData.error || "Erro ao criar usuário",
details: responseData,
}),
};
}
const userData = responseData.user;
console.log(
"[create-user-with-password] Usuário criado com sucesso:",
userData.id
);
console.log(
"[create-user-with-password] Resposta completa:",
JSON.stringify(responseData, null, 2)
);
// A Edge Function já cria o perfil e atribui a role automaticamente
// Retornar sucesso
return {
statusCode: 201,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
success: true,
user: userData,
message: responseData.message || "Usuário criado com sucesso",
}),
};
} catch (error) {
console.error("[create-user-with-password] Erro:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,120 +0,0 @@
/**
* Netlify Function: Create User
* POST /create-user - Cria novo usuário no sistema
* Requer permissão de admin, gestor ou secretaria
* Envia magic link automaticamente para o email
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
// create-user pode ser chamado SEM autenticação (para auto-registro)
// Se houver token, será usado; se não houver, usa apenas anon key
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
console.log(
"[create-user] Recebido body:",
JSON.stringify(body, null, 2)
);
console.log("[create-user] Auth header presente?", !!authHeader);
// Validação dos campos obrigatórios
if (!body.email || !body.full_name) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: email, full_name",
}),
};
}
if (!body.role && (!body.roles || body.roles.length === 0)) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "É necessário fornecer role ou roles",
}),
};
}
// Chama a Edge Function do Supabase para criar usuário
const fetchHeaders: Record<string, string> = {
apikey: SUPABASE_ANON_KEY,
"Content-Type": "application/json",
// Se houver token de usuário autenticado, usa ele; senão usa anon key
Authorization: authHeader || `Bearer ${SUPABASE_ANON_KEY}`,
};
console.log("[create-user] Chamando Supabase com headers:", {
hasAuthHeader: !!authHeader,
hasApikey: !!fetchHeaders.apikey,
authType: authHeader ? "User Token" : "Anon Key",
});
const response = await fetch(`${SUPABASE_URL}/functions/v1/create-user`, {
method: "POST",
headers: fetchHeaders,
body: JSON.stringify(body),
});
const data = await response.json();
console.log("[create-user] Resposta do Supabase:", {
status: response.status,
data: JSON.stringify(data, null, 2),
});
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de create user:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,127 +0,0 @@
/**
* Netlify Function: Delete User (Hard Delete)
* POST /delete-user - Deleta usuário permanentemente
* OPERAÇÃO IRREVERSÍVEL - Use com extremo cuidado!
* Requer permissão de admin ou gestor
* Usa Admin API do Supabase
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_SERVICE_ROLE_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1NDk1NDM2OSwiZXhwIjoyMDcwNTMwMzY5fQ.Dez8PQkV8vWv7VkL_fZe-lY-Xs9P5VptNvRRnhkxoXw";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization, apikey",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({
error: "Token de autenticação é obrigatório",
}),
};
}
const body = JSON.parse(event.body || "{}");
console.log("[delete-user] ATENÇÃO: Tentativa de hard delete:", {
userId: body.userId,
requestedBy: "via Netlify Function",
});
// Validação
if (!body.userId) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "userId é obrigatório",
}),
};
}
// TODO: Aqui deveria verificar se o usuário tem permissão de admin/gestor
// Verificando o token JWT que foi passado
// Deletar usuário via Admin API do Supabase
const response = await fetch(
`${SUPABASE_URL}/auth/v1/admin/users/${body.userId}`,
{
method: "DELETE",
headers: {
apikey: SUPABASE_SERVICE_ROLE_KEY,
Authorization: `Bearer ${SUPABASE_SERVICE_ROLE_KEY}`,
"Content-Type": "application/json",
},
}
);
if (response.ok) {
console.log("[delete-user] Usuário deletado com sucesso:", body.userId);
return {
statusCode: 200,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
success: true,
message: "Usuário deletado permanentemente",
userId: body.userId,
}),
};
}
const errorData = await response.json();
console.error("[delete-user] Erro ao deletar:", errorData);
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
error: errorData.msg || errorData.message || "Erro ao deletar usuário",
details: errorData,
}),
};
} catch (error) {
console.error("[delete-user] Erro:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,217 +0,0 @@
/**
* Netlify Function: doctor-availability
*
* Proxy para operações de disponibilidade dos médicos
* GET: Lista disponibilidades
* POST: Criar disponibilidade
* PATCH: Atualizar disponibilidade
* DELETE: Deletar disponibilidade
*/
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_API_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export default async (req: Request) => {
// Permitir CORS
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
try {
const url = new URL(req.url);
const authHeader = req.headers.get("Authorization");
// Extrair ID do path se existir
const pathParts = url.pathname.split("/");
const availabilityId = pathParts[pathParts.length - 1];
// GET: Listar disponibilidades
if (req.method === "GET") {
const select = url.searchParams.get("select") || "*";
const doctor_id = url.searchParams.get("doctor_id");
const active = url.searchParams.get("active");
const queryParams = new URLSearchParams();
queryParams.append("select", select);
if (doctor_id) queryParams.append("doctor_id", `eq.${doctor_id}`);
if (active !== null) queryParams.append("active", `eq.${active}`);
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_availability?${queryParams}`;
const headers: HeadersInit = {
apikey: SUPABASE_API_KEY,
"Content-Type": "application/json",
};
if (authHeader) {
headers["Authorization"] = authHeader;
}
const response = await fetch(supabaseUrl, {
method: "GET",
headers,
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
}
// POST: Criar disponibilidade
if (req.method === "POST") {
const body = await req.json();
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_availability`;
const headers: HeadersInit = {
apikey: SUPABASE_API_KEY,
"Content-Type": "application/json",
Prefer: "return=representation",
};
if (authHeader) {
headers["Authorization"] = authHeader;
}
const response = await fetch(supabaseUrl, {
method: "POST",
headers,
body: JSON.stringify(body),
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
}
// PATCH: Atualizar disponibilidade
if (req.method === "PATCH") {
if (!availabilityId || availabilityId === "doctor-availability") {
return new Response(
JSON.stringify({ error: "Availability ID is required" }),
{
status: 400,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
}
);
}
const body = await req.json();
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_availability?id=eq.${availabilityId}`;
const headers: HeadersInit = {
apikey: SUPABASE_API_KEY,
"Content-Type": "application/json",
Prefer: "return=representation",
};
if (authHeader) {
headers["Authorization"] = authHeader;
}
const response = await fetch(supabaseUrl, {
method: "PATCH",
headers,
body: JSON.stringify(body),
});
const data = await response.json();
const result = Array.isArray(data) && data.length > 0 ? data[0] : data;
return new Response(JSON.stringify(result), {
status: response.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
}
// DELETE: Deletar disponibilidade
if (req.method === "DELETE") {
if (!availabilityId || availabilityId === "doctor-availability") {
return new Response(
JSON.stringify({ error: "Availability ID is required" }),
{
status: 400,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
}
);
}
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_availability?id=eq.${availabilityId}`;
const headers: HeadersInit = {
apikey: SUPABASE_API_KEY,
"Content-Type": "application/json",
};
if (authHeader) {
headers["Authorization"] = authHeader;
}
const response = await fetch(supabaseUrl, {
method: "DELETE",
headers,
});
return new Response(null, {
status: response.status,
headers: {
"Access-Control-Allow-Origin": "*",
},
});
}
// Método não suportado
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
} catch (error) {
console.error("Error in doctor-availability function:", error);
return new Response(
JSON.stringify({
error: "Internal server error",
details: error instanceof Error ? error.message : "Unknown error",
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
}
);
}
};

View File

@ -1,169 +0,0 @@
/**
* Netlify Function: doctor-exceptions
*
* Proxy para operações de exceções na agenda dos médicos
* GET: Lista exceções
* POST: Criar exceção
* DELETE: Deletar exceção
*/
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_API_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export default async (req: Request) => {
// Permitir CORS
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
try {
const url = new URL(req.url);
const authHeader = req.headers.get("Authorization");
// Extrair ID do path se existir
const pathParts = url.pathname.split("/");
const exceptionId = pathParts[pathParts.length - 1];
// GET: Listar exceções
if (req.method === "GET") {
const select = url.searchParams.get("select") || "*";
const doctor_id = url.searchParams.get("doctor_id");
const date = url.searchParams.get("date");
const queryParams = new URLSearchParams();
queryParams.append("select", select);
if (doctor_id) queryParams.append("doctor_id", `eq.${doctor_id}`);
if (date) queryParams.append("date", `eq.${date}`);
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_exceptions?${queryParams}`;
const headers: HeadersInit = {
apikey: SUPABASE_API_KEY,
"Content-Type": "application/json",
};
if (authHeader) {
headers["Authorization"] = authHeader;
}
const response = await fetch(supabaseUrl, {
method: "GET",
headers,
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
}
// POST: Criar exceção
if (req.method === "POST") {
const body = await req.json();
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_exceptions`;
const headers: HeadersInit = {
apikey: SUPABASE_API_KEY,
"Content-Type": "application/json",
Prefer: "return=representation",
};
if (authHeader) {
headers["Authorization"] = authHeader;
}
const response = await fetch(supabaseUrl, {
method: "POST",
headers,
body: JSON.stringify(body),
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
}
// DELETE: Deletar exceção
if (req.method === "DELETE") {
if (!exceptionId || exceptionId === "doctor-exceptions") {
return new Response(
JSON.stringify({ error: "Exception ID is required" }),
{
status: 400,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
}
);
}
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_exceptions?id=eq.${exceptionId}`;
const headers: HeadersInit = {
apikey: SUPABASE_API_KEY,
"Content-Type": "application/json",
};
if (authHeader) {
headers["Authorization"] = authHeader;
}
const response = await fetch(supabaseUrl, {
method: "DELETE",
headers,
});
return new Response(null, {
status: response.status,
headers: {
"Access-Control-Allow-Origin": "*",
},
});
}
// Método não suportado
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
} catch (error) {
console.error("Error in doctor-exceptions function:", error);
return new Response(
JSON.stringify({
error: "Internal server error",
details: error instanceof Error ? error.message : "Unknown error",
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
}
);
}
};

View File

@ -1,237 +0,0 @@
/**
* Netlify Function: Doctors CRUD
* GET /rest/v1/doctors - Lista médicos
* GET /rest/v1/doctors/{id} - Busca por ID
* POST /rest/v1/doctors - Cria médico
* PATCH /rest/v1/doctors/{id} - Atualiza médico
* DELETE /rest/v1/doctors/{id} - Deleta médico
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Extrai ID da URL se houver (doctors/123 ou doctors?id=123)
const pathParts = event.path.split("/");
const doctorId =
pathParts[pathParts.length - 1] !== "doctors"
? pathParts[pathParts.length - 1]
: null;
// GET - Listar ou buscar por ID
if (event.httpMethod === "GET") {
let url = `${SUPABASE_URL}/rest/v1/doctors`;
if (doctorId && doctorId !== "doctors") {
// Buscar por ID específico
url += `?id=eq.${doctorId}&select=*`;
} else if (event.queryStringParameters) {
// Adiciona filtros da query string
const params = new URLSearchParams(
event.queryStringParameters as Record<string, string>
);
url += `?${params.toString()}`;
// Adiciona select=* se não tiver
if (!params.has("select")) {
url += url.includes("?") ? "&select=*" : "?select=*";
}
} else {
url += "?select=*";
}
const response = await fetch(url, {
method: "GET",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
let data = await response.json();
// Se buscar por ID, retorna o objeto diretamente (não array)
if (
doctorId &&
doctorId !== "doctors" &&
Array.isArray(data) &&
data.length > 0
) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// POST - Criar médico
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
if (
!body.crm ||
!body.crm_uf ||
!body.full_name ||
!body.cpf ||
!body.email
) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: crm, crm_uf, full_name, cpf, email",
}),
};
}
const response = await fetch(`${SUPABASE_URL}/rest/v1/doctors`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
});
let data = await response.json();
// Supabase retorna array, pega o primeiro
if (Array.isArray(data) && data.length > 0) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// PATCH - Atualizar médico
if (event.httpMethod === "PATCH") {
if (!doctorId || doctorId === "doctors") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do médico é obrigatório" }),
};
}
const body = JSON.parse(event.body || "{}");
const response = await fetch(
`${SUPABASE_URL}/rest/v1/doctors?id=eq.${doctorId}`,
{
method: "PATCH",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
}
);
let data = await response.json();
if (Array.isArray(data) && data.length > 0) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// DELETE - Deletar médico
if (event.httpMethod === "DELETE") {
if (!doctorId || doctorId === "doctors") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do médico é obrigatório" }),
};
}
const response = await fetch(
`${SUPABASE_URL}/rest/v1/doctors?id=eq.${doctorId}`,
{
method: "DELETE",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
}
);
return {
statusCode: response.status,
headers,
body: "",
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de médicos:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,95 +0,0 @@
/**
* Netlify Function: Get Available Slots
* POST /functions/v1/get-available-slots - Busca horários disponíveis
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
// Validação dos campos obrigatórios
if (!body.doctor_id || !body.start_date || !body.end_date) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: doctor_id, start_date, end_date",
}),
};
}
const response = await fetch(
`${SUPABASE_URL}/functions/v1/get-available-slots`,
{
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
}
);
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de available slots:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,226 +0,0 @@
/**
* Netlify Function: Patients CRUD
* GET /rest/v1/patients - Lista pacientes
* GET /rest/v1/patients/{id} - Busca por ID
* POST /rest/v1/patients - Cria paciente
* PATCH /rest/v1/patients/{id} - Atualiza paciente
* DELETE /rest/v1/patients/{id} - Deleta paciente
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Extrai ID da URL se houver
const pathParts = event.path.split("/");
const patientId =
pathParts[pathParts.length - 1] !== "patients"
? pathParts[pathParts.length - 1]
: null;
// GET - Listar ou buscar por ID
if (event.httpMethod === "GET") {
let url = `${SUPABASE_URL}/rest/v1/patients`;
if (patientId && patientId !== "patients") {
url += `?id=eq.${patientId}&select=*`;
} else if (event.queryStringParameters) {
const params = new URLSearchParams(
event.queryStringParameters as Record<string, string>
);
url += `?${params.toString()}`;
if (!params.has("select")) {
url += url.includes("?") ? "&select=*" : "?select=*";
}
} else {
url += "?select=*";
}
const response = await fetch(url, {
method: "GET",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
let data = await response.json();
if (
patientId &&
patientId !== "patients" &&
Array.isArray(data) &&
data.length > 0
) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// POST - Criar paciente
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
if (!body.full_name || !body.cpf || !body.email || !body.phone_mobile) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: full_name, cpf, email, phone_mobile",
}),
};
}
const response = await fetch(`${SUPABASE_URL}/rest/v1/patients`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
});
let data = await response.json();
if (Array.isArray(data) && data.length > 0) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// PATCH - Atualizar paciente
if (event.httpMethod === "PATCH") {
if (!patientId || patientId === "patients") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do paciente é obrigatório" }),
};
}
const body = JSON.parse(event.body || "{}");
const response = await fetch(
`${SUPABASE_URL}/rest/v1/patients?id=eq.${patientId}`,
{
method: "PATCH",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
}
);
let data = await response.json();
if (Array.isArray(data) && data.length > 0) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// DELETE - Deletar paciente
if (event.httpMethod === "DELETE") {
if (!patientId || patientId === "patients") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do paciente é obrigatório" }),
};
}
const response = await fetch(
`${SUPABASE_URL}/rest/v1/patients?id=eq.${patientId}`,
{
method: "DELETE",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
}
);
return {
statusCode: response.status,
headers,
body: "",
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de pacientes:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,155 +0,0 @@
/**
* Netlify Function: Profiles
* GET /rest/v1/profiles - Lista perfis
* GET /rest/v1/profiles/{id} - Busca por ID
* PATCH /rest/v1/profiles/{id} - Atualiza avatar_url
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, PATCH, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Extrai ID da URL se houver
const pathParts = event.path.split("/");
const profileId =
pathParts[pathParts.length - 1] !== "profiles"
? pathParts[pathParts.length - 1]
: null;
// GET - Listar ou buscar por ID
if (event.httpMethod === "GET") {
let url = `${SUPABASE_URL}/rest/v1/profiles`;
if (profileId && profileId !== "profiles") {
url += `?id=eq.${profileId}&select=*`;
} else if (event.queryStringParameters) {
const params = new URLSearchParams(
event.queryStringParameters as Record<string, string>
);
url += `?${params.toString()}`;
if (!params.has("select")) {
url += url.includes("?") ? "&select=*" : "?select=*";
}
} else {
url += "?select=*";
}
const response = await fetch(url, {
method: "GET",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
let data = await response.json();
if (
profileId &&
profileId !== "profiles" &&
Array.isArray(data) &&
data.length > 0
) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// PATCH - Atualizar avatar_url
if (event.httpMethod === "PATCH") {
if (!profileId || profileId === "profiles") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do perfil é obrigatório" }),
};
}
const body = JSON.parse(event.body || "{}");
const response = await fetch(
`${SUPABASE_URL}/rest/v1/profiles?id=eq.${profileId}`,
{
method: "PATCH",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
}
);
let data = await response.json();
if (Array.isArray(data) && data.length > 0) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de perfis:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,97 +0,0 @@
/**
* Netlify Function: Register Patient (Public)
* POST /register-patient - Registro público de paciente
* Não requer autenticação - função pública
* Validações rigorosas (CPF, rate limiting, rollback)
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
console.log(
"[register-patient] Recebido body:",
JSON.stringify(body, null, 2)
);
// Validação dos campos obrigatórios
if (!body.email || !body.full_name || !body.cpf || !body.phone_mobile) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: email, full_name, cpf, phone_mobile",
}),
};
}
// Chama a Edge Function pública do Supabase
const response = await fetch(
`${SUPABASE_URL}/functions/v1/register-patient`,
{
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
}
);
const data = await response.json();
console.log("[register-patient] Resposta do Supabase:", {
status: response.status,
data: JSON.stringify(data, null, 2),
});
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("[register-patient] Erro na API:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno do servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,197 +0,0 @@
/**
* Netlify Function: Reports
* GET /rest/v1/reports - Lista relatórios
* GET /rest/v1/reports/{id} - Busca por ID
* POST /rest/v1/reports - Cria relatório
* PATCH /rest/v1/reports/{id} - Atualiza relatório
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Extrai ID da URL se houver
const pathParts = event.path.split("/");
const reportId =
pathParts[pathParts.length - 1] !== "reports"
? pathParts[pathParts.length - 1]
: null;
// GET - Listar ou buscar por ID
if (event.httpMethod === "GET") {
let url = `${SUPABASE_URL}/rest/v1/reports`;
if (reportId && reportId !== "reports") {
url += `?id=eq.${reportId}&select=*`;
} else if (event.queryStringParameters) {
const params = new URLSearchParams(
event.queryStringParameters as Record<string, string>
);
url += `?${params.toString()}`;
if (!params.has("select")) {
url += url.includes("?") ? "&select=*" : "?select=*";
}
} else {
url += "?select=*";
}
const response = await fetch(url, {
method: "GET",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
let data = await response.json();
if (
reportId &&
reportId !== "reports" &&
Array.isArray(data) &&
data.length > 0
) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// POST - Criar relatório
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
if (!body.patient_id) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campo obrigatório: patient_id",
}),
};
}
const response = await fetch(`${SUPABASE_URL}/rest/v1/reports`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
});
let data = await response.json();
if (Array.isArray(data) && data.length > 0) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// PATCH - Atualizar relatório
if (event.httpMethod === "PATCH") {
if (!reportId || reportId === "reports") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do relatório é obrigatório" }),
};
}
const body = JSON.parse(event.body || "{}");
const response = await fetch(
`${SUPABASE_URL}/rest/v1/reports?id=eq.${reportId}`,
{
method: "PATCH",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
}
);
let data = await response.json();
if (Array.isArray(data) && data.length > 0) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de relatórios:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,116 +0,0 @@
/**
* Netlify Function: Request Password Reset
* POST /request-password-reset - Solicita reset de senha via email (público)
* Não requer autenticação - endpoint público
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
interface PasswordResetRequest {
email: string;
redirect_url?: string;
}
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const body: PasswordResetRequest = JSON.parse(event.body || "{}");
console.log("[request-password-reset] Recebido:", {
email: body.email,
hasRedirectUrl: !!body.redirect_url,
});
if (!body.email) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "Email é obrigatório" }),
};
}
// Chama a API do Supabase para enviar email de reset
const response = await fetch(`${SUPABASE_URL}/auth/v1/recover`, {
method: "POST",
headers: {
"Content-Type": "application/json",
apikey: SUPABASE_ANON_KEY,
},
body: JSON.stringify({
email: body.email,
options: {
redirectTo:
body.redirect_url ||
"https://mediconnectbrasil.netlify.app/reset-password",
},
}),
});
const data = await response.json();
console.log("[request-password-reset] Resposta Supabase:", {
status: response.status,
data,
});
// Supabase sempre retorna 200 mesmo se o email não existir (por segurança)
if (response.ok) {
return {
statusCode: 200,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
success: true,
message:
"Email de reset de senha enviado com sucesso. Verifique sua caixa de entrada.",
}),
};
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("[request-password-reset] Erro:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro ao solicitar reset de senha",
details: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,93 +0,0 @@
/**
* Netlify Function: Send SMS
* POST /functions/v1/send-sms - Envia SMS via Twilio
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
// Validação dos campos obrigatórios
if (!body.phone_number || !body.message) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: phone_number, message",
}),
};
}
// Chama a função Supabase de enviar SMS
const response = await fetch(`${SUPABASE_URL}/functions/v1/send-sms`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de SMS:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,93 +0,0 @@
/**
* Netlify Function: User Info By ID
* POST /user-info-by-id - Retorna dados de usuário específico (apenas admin/gestor)
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
const body = JSON.parse(event.body || "{}");
if (!body.user_id) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "Campo obrigatório: user_id" }),
};
}
// Chama a Edge Function do Supabase
const response = await fetch(
`${SUPABASE_URL}/functions/v1/user-info-by-id`,
{
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
}
);
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro na API de user-info-by-id:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,81 +0,0 @@
/**
* Netlify Function: User Info
* GET /functions/v1/user-info - Retorna informações completas do usuário autenticado
* Inclui: user, profile, roles e permissions calculadas
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Aceita tanto POST quanto GET para compatibilidade
if (event.httpMethod === "POST" || event.httpMethod === "GET") {
// Chama a Edge Function do Supabase (POST conforme doc 21/10/2025)
const response = await fetch(`${SUPABASE_URL}/functions/v1/user-info`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
},
});
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de user-info:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,161 +0,0 @@
/**
* Netlify Function: User Roles
* GET /rest/v1/user_roles - Lista roles de usuários
* POST /rest/v1/user_roles - Adiciona role a um usuário
* DELETE /rest/v1/user_roles - Remove role de um usuário
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
if (event.httpMethod === "GET") {
let url = `${SUPABASE_URL}/rest/v1/user_roles?select=*`;
if (event.queryStringParameters) {
const params = new URLSearchParams(
event.queryStringParameters as Record<string, string>
);
const paramsStr = params.toString();
if (paramsStr) {
url += `&${paramsStr}`;
}
}
const response = await fetch(url, {
method: "GET",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
if (event.httpMethod === "POST") {
// Adicionar nova role para um usuário
const body = JSON.parse(event.body || "{}");
if (!body.user_id || !body.role) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "user_id e role são obrigatórios" }),
};
}
const response = await fetch(`${SUPABASE_URL}/rest/v1/user_roles`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify({
user_id: body.user_id,
role: body.role,
}),
});
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
if (event.httpMethod === "DELETE") {
// Remover role de um usuário
const params = event.queryStringParameters;
if (!params?.user_id || !params?.role) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "user_id e role são obrigatórios" }),
};
}
const url = `${SUPABASE_URL}/rest/v1/user_roles?user_id=eq.${params.user_id}&role=eq.${params.role}`;
const response = await fetch(url, {
method: "DELETE",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({ success: true }),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de user roles:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -0,0 +1,234 @@
import { useState, useRef, useEffect } from 'react';
import { MessageCircle, X, Send } from 'lucide-react';
interface Message {
id: number;
text: string;
sender: 'user' | 'bot';
timestamp: Date;
}
export function Chatbot() {
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([
{
id: 1,
text: 'Olá! Como posso ajudar você hoje?',
sender: 'bot',
timestamp: new Date(),
},
]);
const [inputMessage, setInputMessage] = useState('');
const [isTyping, setIsTyping] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleSend = async () => {
if (!inputMessage.trim()) return;
const userMessage: Message = {
id: messages.length + 1,
text: inputMessage,
sender: 'user',
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMessage]);
setInputMessage('');
setIsTyping(true);
// Simular resposta do bot
setTimeout(() => {
const botResponse = getBotResponse(inputMessage.toLowerCase());
const botMessage: Message = {
id: messages.length + 2,
text: botResponse,
sender: 'bot',
timestamp: new Date(),
};
setMessages((prev) => [...prev, botMessage]);
setIsTyping(false);
}, 1000);
};
const getBotResponse = (input: string): string => {
if (input.includes('agendar') || input.includes('consulta') || input.includes('marcar')) {
return 'Para agendar uma consulta, acesse o painel do paciente e clique em "Agendar Consulta". Você poderá escolher o médico, data e horário disponível.';
}
if (input.includes('cancelar') || input.includes('remarcar')) {
return 'Para cancelar ou remarcar uma consulta, acesse "Minhas Consultas" no painel do paciente e clique na consulta desejada.';
}
if (input.includes('pagamento') || input.includes('pagar')) {
return 'Aceitamos pagamento via PIX, cartão de crédito e débito. O pagamento pode ser realizado no momento da consulta ou através do nosso sistema online.';
}
if (input.includes('senha') || input.includes('esqueci')) {
return 'Para redefinir sua senha, clique em "Esqueci minha senha" na tela de login e siga as instruções enviadas para seu e-mail.';
}
if (input.includes('exame') || input.includes('resultado')) {
return 'Os resultados de exames ficam disponíveis no menu "Meus Laudos" do painel do paciente. Você receberá uma notificação quando estiverem prontos.';
}
if (input.includes('horário') || input.includes('funciona')) {
return 'Nosso atendimento funciona de segunda a sexta das 8h às 18h, e sábados das 8h às 12h.';
}
if (input.includes('telemedicina') || input.includes('online')) {
return 'Sim, oferecemos consultas por telemedicina! Ao agendar, selecione a opção "Teleconsulta" e você receberá o link para a videochamada.';
}
if (input.includes('prontuário') || input.includes('histórico')) {
return 'Seu histórico médico completo está disponível no menu "Meu Prontuário" no painel do paciente.';
}
if (input.includes('suporte') || input.includes('ajuda') || input.includes('contato')) {
return 'Para suporte adicional, entre em contato conosco:\n📞 Telefone: (11) 1234-5678\n📧 Email: suporte@mediconnect.com.br\n💬 WhatsApp: (11) 98765-4321';
}
return 'Desculpe, não entendi sua pergunta. Você pode perguntar sobre:\n• Agendar consultas\n• Cancelar/remarcar consultas\n• Pagamentos\n• Resultados de exames\n• Redefinir senha\n• Horário de funcionamento\n• Telemedicina\n• Contato/suporte';
};
const quickReplies = [
'Como agendar consulta?',
'Horário de funcionamento',
'Resultados de exames',
'Esqueci minha senha',
];
return (
<>
{/* Botão flutuante */}
{!isOpen && (
<button
onClick={() => setIsOpen(true)}
className="fixed bottom-6 right-6 bg-[#00a8a8] hover:bg-[#008c8c] text-white rounded-full p-4 shadow-lg transition-all duration-300 hover:scale-110 z-50"
aria-label="Abrir chat"
>
<MessageCircle size={28} />
</button>
)}
{/* Janela do chat */}
{isOpen && (
<div className="fixed bottom-6 right-6 w-96 h-[600px] bg-white rounded-2xl shadow-2xl flex flex-col z-50 border border-gray-200">
{/* Header */}
<div className="bg-gradient-to-r from-[#00a8a8] to-[#008c8c] text-white p-4 rounded-t-2xl flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="bg-white/20 p-2 rounded-full">
<MessageCircle size={24} />
</div>
<div>
<h3 className="font-semibold text-lg">Assistente Virtual</h3>
<p className="text-sm text-white/80">Online</p>
</div>
</div>
<button
onClick={() => setIsOpen(false)}
className="hover:bg-white/20 p-2 rounded-full transition-colors"
aria-label="Fechar chat"
>
<X size={24} />
</button>
</div>
{/* Mensagens */}
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${message.sender === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[80%] rounded-2xl px-4 py-2 ${
message.sender === 'user'
? 'bg-[#00a8a8] text-white rounded-br-sm'
: 'bg-white text-gray-800 rounded-bl-sm shadow-sm border border-gray-200'
}`}
>
<p className="text-sm whitespace-pre-line">{message.text}</p>
<p
className={`text-xs mt-1 ${
message.sender === 'user' ? 'text-white/70' : 'text-gray-500'
}`}
>
{message.timestamp.toLocaleTimeString('pt-BR', {
hour: '2-digit',
minute: '2-digit',
})}
</p>
</div>
</div>
))}
{isTyping && (
<div className="flex justify-start">
<div className="bg-white rounded-2xl rounded-bl-sm px-4 py-3 shadow-sm border border-gray-200">
<div className="flex gap-1">
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></span>
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></span>
<span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.4s' }}></span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Respostas rápidas */}
{messages.length === 1 && (
<div className="px-4 py-2 border-t border-gray-200 bg-white">
<p className="text-xs text-gray-600 mb-2">Perguntas frequentes:</p>
<div className="flex flex-wrap gap-2">
{quickReplies.map((reply, index) => (
<button
key={index}
onClick={() => {
setInputMessage(reply);
handleSend();
}}
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-1.5 rounded-full transition-colors"
>
{reply}
</button>
))}
</div>
</div>
)}
{/* Input */}
<div className="p-4 border-t border-gray-200 bg-white rounded-b-2xl">
<div className="flex gap-2">
<input
type="text"
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
placeholder="Digite sua mensagem..."
className="flex-1 border border-gray-300 rounded-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-[#00a8a8] focus:border-transparent text-sm"
/>
<button
onClick={handleSend}
disabled={!inputMessage.trim()}
className="bg-[#00a8a8] hover:bg-[#008c8c] disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-full p-2 transition-colors"
aria-label="Enviar mensagem"
>
<Send size={20} />
</button>
</div>
</div>
</div>
)}
</>
);
}

View File

@ -14,6 +14,7 @@ import {
Headphones, Headphones,
ArrowLeft, ArrowLeft,
} from "lucide-react"; } from "lucide-react";
import { Chatbot } from "../components/Chatbot";
interface FAQ { interface FAQ {
question: string; question: string;
@ -404,6 +405,9 @@ const CentralAjuda: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
{/* Chatbot */}
<Chatbot />
</div> </div>
); );
}; };

View File

@ -14,6 +14,7 @@ import {
Headphones, Headphones,
ArrowLeft, ArrowLeft,
} from "lucide-react"; } from "lucide-react";
import { Chatbot } from "../components/Chatbot";
interface FAQ { interface FAQ {
question: string; question: string;
@ -408,6 +409,9 @@ const CentralAjudaMedico: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
{/* Chatbot */}
<Chatbot />
</div> </div>
); );
}; };

View File

@ -22,7 +22,7 @@ const LoginPaciente: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { loginPaciente } = useAuth(); const { loginComEmailSenha } = useAuth();
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -31,48 +31,19 @@ const LoginPaciente: React.FC = () => {
try { try {
console.log("[LoginPaciente] Fazendo login com email:", formData.email); console.log("[LoginPaciente] Fazendo login com email:", formData.email);
// Fazer login via API Supabase const ok = await loginComEmailSenha(formData.email, formData.senha);
await authService.login({
email: formData.email,
password: formData.senha,
});
console.log("[LoginPaciente] Login bem-sucedido!");
// Buscar dados do paciente da API
const pacientes = await patientService.list();
const paciente = pacientes.find((p: any) => p.email === formData.email);
console.log("[LoginPaciente] Paciente encontrado:", paciente);
if (paciente) {
console.log("[LoginPaciente] Paciente encontrado:", {
id: paciente.id,
nome: paciente.full_name,
email: paciente.email,
});
const ok = await loginPaciente({
id: paciente.id,
nome: paciente.full_name,
email: paciente.email,
});
if (ok) { if (ok) {
console.log("[LoginPaciente] Navegando para /acompanhamento"); console.log("[LoginPaciente] Login bem-sucedido! Navegando para /acompanhamento");
toast.success("Login realizado com sucesso!");
navigate("/acompanhamento"); navigate("/acompanhamento");
} else { } else {
console.error("[LoginPaciente] loginPaciente retornou false"); console.error("[LoginPaciente] loginComEmailSenha retornou false");
toast.error("Erro ao processar login"); toast.error("Credenciais inválidas ou usuário sem permissão");
}
} else {
console.log("[LoginPaciente] Paciente não encontrado na lista");
toast.error(
"Dados do paciente não encontrados. Entre em contato com o suporte."
);
} }
} catch (error) { } catch (error) {
console.error("[LoginPaciente] Erro no login:", error); console.error("[LoginPaciente] Erro no login:", error);
toast.error("Erro ao fazer login. Tente novamente."); toast.error("Erro ao fazer login. Verifique suas credenciais.");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -158,68 +129,6 @@ const LoginPaciente: React.FC = () => {
} }
}; };
// Login LOCAL: cria uma sessão de paciente sem chamar a API
const handleLoginLocal = async () => {
const email = formData.email.trim();
const senha = formData.senha;
console.log("[LoginPaciente] Login local - tentando com API primeiro");
// Tentar fazer login via API mesmo no modo "local"
setLoading(true);
try {
// Fazer login via API Supabase
await authService.login({
email: email,
password: senha,
});
console.log("[LoginPaciente] Login via API bem-sucedido!");
// Buscar dados do paciente da API
const pacientes = await patientService.list();
const paciente = pacientes.find((p: any) => p.email === email);
if (paciente) {
console.log(
"[LoginPaciente] Paciente encontrado na API:",
paciente.full_name
);
const ok = await loginPaciente({
id: paciente.id,
nome: paciente.full_name,
email: paciente.email,
});
if (ok) {
navigate("/acompanhamento");
} else {
toast.error("Erro ao processar login");
}
} else {
console.log(
"[LoginPaciente] Paciente não encontrado na API, usando dados locais"
);
const ok = await loginPaciente({
id: email,
nome: email.split("@")[0],
email: email,
});
if (ok) {
navigate("/acompanhamento");
} else {
toast.error("Erro ao processar login");
}
}
} catch (err) {
console.error("[LoginPaciente] Erro no login:", err);
toast.error("Erro ao fazer login");
} finally {
setLoading(false);
}
};
return ( return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-white dark:from-gray-900 dark:to-gray-950 flex items-center justify-center p-4 transition-colors"> <div className="min-h-screen bg-gradient-to-br from-blue-50 to-white dark:from-gray-900 dark:to-gray-950 flex items-center justify-center p-4 transition-colors">
<div className="max-w-md w-full"> <div className="max-w-md w-full">
@ -335,8 +244,7 @@ const LoginPaciente: React.FC = () => {
**/} **/}
<button <button
type="button" type="submit"
onClick={handleLoginLocal}
disabled={loading} disabled={loading}
className="w-full bg-gradient-to-r from-blue-700 to-blue-400 text-white py-3 px-4 rounded-lg font-medium hover:from-blue-800 hover:to-blue-500 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2" className="w-full bg-gradient-to-r from-blue-700 to-blue-400 text-white py-3 px-4 rounded-lg font-medium hover:from-blue-800 hover:to-blue-500 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
> >

View File

@ -72,8 +72,11 @@ class AppointmentService {
* Busca agendamento por ID * Busca agendamento por ID
*/ */
async getById(id: string): Promise<Appointment> { async getById(id: string): Promise<Appointment> {
const response = await apiClient.get<Appointment>(`${this.basePath}/${id}`); const response = await apiClient.get<Appointment[]>(`${this.basePath}?id=eq.${id}`);
return response.data; if (response.data && response.data.length > 0) {
return response.data[0];
}
throw new Error("Agendamento não encontrado");
} }
/** /**
@ -89,18 +92,21 @@ class AppointmentService {
* Atualiza agendamento existente * Atualiza agendamento existente
*/ */
async update(id: string, data: UpdateAppointmentInput): Promise<Appointment> { async update(id: string, data: UpdateAppointmentInput): Promise<Appointment> {
const response = await apiClient.patch<Appointment>( const response = await apiClient.patch<Appointment[]>(
`${this.basePath}/${id}`, `${this.basePath}?id=eq.${id}`,
data data
); );
return response.data; if (response.data && response.data.length > 0) {
return response.data[0];
}
throw new Error("Agendamento não encontrado");
} }
/** /**
* Deleta agendamento * Deleta agendamento
*/ */
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
await apiClient.delete(`${this.basePath}/${id}`); await apiClient.delete(`${this.basePath}?id=eq.${id}`);
} }
} }

View File

@ -35,8 +35,11 @@ class ReportService {
* Busca relatório por ID * Busca relatório por ID
*/ */
async getById(id: string): Promise<Report> { async getById(id: string): Promise<Report> {
const response = await apiClient.get<Report>(`${this.basePath}/${id}`); const response = await apiClient.get<Report[]>(`${this.basePath}?id=eq.${id}`);
return response.data; if (response.data && response.data.length > 0) {
return response.data[0];
}
throw new Error("Relatório não encontrado");
} }
/** /**
@ -53,11 +56,14 @@ class ReportService {
* Nota: order_number não pode ser modificado * Nota: order_number não pode ser modificado
*/ */
async update(id: string, data: UpdateReportInput): Promise<Report> { async update(id: string, data: UpdateReportInput): Promise<Report> {
const response = await apiClient.patch<Report>( const response = await apiClient.patch<Report[]>(
`${this.basePath}/${id}`, `${this.basePath}?id=eq.${id}`,
data data
); );
return response.data; if (response.data && response.data.length > 0) {
return response.data[0];
}
throw new Error("Relatório não encontrado");
} }
} }