fix-magic-link-endpoint

This commit is contained in:
João Gustavo 2025-10-16 23:08:48 -03:00
parent 8f443b63e5
commit 26a20225f6
4 changed files with 93 additions and 14 deletions

View File

@ -3,6 +3,7 @@ import { useState } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import Link from 'next/link' import Link from 'next/link'
import { useAuth } from '@/hooks/useAuth' import { useAuth } from '@/hooks/useAuth'
import { sendMagicLink } from '@/lib/api'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
@ -12,6 +13,9 @@ import { AuthenticationError } from '@/lib/auth'
export default function LoginAdminPage() { export default function LoginAdminPage() {
const [credentials, setCredentials] = useState({ email: '', password: '' }) const [credentials, setCredentials] = useState({ email: '', password: '' })
const [error, setError] = useState('') const [error, setError] = useState('')
const [magicMessage, setMagicMessage] = useState('')
const [magicError, setMagicError] = useState('')
const [magicLoading, setMagicLoading] = useState(false)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const router = useRouter() const router = useRouter()
const { login } = useAuth() const { login } = useAuth()
@ -44,6 +48,27 @@ export default function LoginAdminPage() {
} }
} }
const handleSendMagicLink = async () => {
if (!credentials.email) {
setMagicError('Por favor, preencha o email antes de solicitar o magic link.')
return
}
setMagicLoading(true)
setMagicError('')
setMagicMessage('')
try {
const res = await sendMagicLink(credentials.email, { target: 'admin' })
setMagicMessage(res?.message ?? 'Magic link enviado. Verifique seu email.')
} catch (err: any) {
console.error('[MAGIC-LINK ADMIN] erro ao enviar:', err)
setMagicError(err?.message ?? String(err))
} finally {
setMagicLoading(false)
}
}
return ( return (
<div className="min-h-screen flex items-center justify-center bg-background py-12 px-4 sm:px-6 lg:px-8"> <div className="min-h-screen flex items-center justify-center bg-background py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8"> <div className="max-w-md w-full space-y-8">
@ -108,6 +133,25 @@ export default function LoginAdminPage() {
{loading ? 'Entrando...' : 'Entrar no Sistema Administrativo'} {loading ? 'Entrando...' : 'Entrar no Sistema Administrativo'}
</Button> </Button>
</form> </form>
<div className="mt-4 space-y-2">
<div className="text-sm text-muted-foreground mb-2">Ou entre usando um magic link (sem senha)</div>
{magicError && (
<Alert variant="destructive">
<AlertDescription>{magicError}</AlertDescription>
</Alert>
)}
{magicMessage && (
<Alert>
<AlertDescription>{magicMessage}</AlertDescription>
</Alert>
)}
<Button className="w-full" onClick={handleSendMagicLink} disabled={magicLoading}>
{magicLoading ? 'Enviando magic link...' : 'Enviar magic link'}
</Button>
</div>
<div className="mt-4 text-center"> <div className="mt-4 text-center">
<Button variant="outline" asChild className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200"> <Button variant="outline" asChild className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200">

View File

@ -66,7 +66,7 @@ export default function LoginPacientePage() {
setMagicMessage('') setMagicMessage('')
try { try {
const res = await sendMagicLink(credentials.email, { emailRedirectTo: `${window.location.origin}/` }) const res = await sendMagicLink(credentials.email, { target: 'paciente' })
setMagicMessage(res?.message ?? 'Magic link enviado. Verifique seu email.') setMagicMessage(res?.message ?? 'Magic link enviado. Verifique seu email.')
} catch (err: any) { } catch (err: any) {
console.error('[MAGIC-LINK PACIENTE] erro ao enviar:', err) console.error('[MAGIC-LINK PACIENTE] erro ao enviar:', err)

View File

@ -69,7 +69,7 @@ export default function LoginPage() {
setMagicMessage('') setMagicMessage('')
try { try {
const res = await sendMagicLink(credentials.email, { emailRedirectTo: `${window.location.origin}/` }) const res = await sendMagicLink(credentials.email, { target: 'medico' })
setMagicMessage(res?.message ?? 'Magic link enviado. Verifique seu email.') setMagicMessage(res?.message ?? 'Magic link enviado. Verifique seu email.')
} catch (err: any) { } catch (err: any) {
console.error('[MAGIC-LINK] erro ao enviar:', err) console.error('[MAGIC-LINK] erro ao enviar:', err)

View File

@ -603,6 +603,27 @@ export async function deletarExcecao(id: string): Promise<void> {
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? ENV_CONFIG.SUPABASE_URL; const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? ENV_CONFIG.SUPABASE_URL;
const REST = `${API_BASE}/rest/v1`; const REST = `${API_BASE}/rest/v1`;
// Helper to build/normalize redirect URLs
function buildRedirectUrl(target?: 'paciente' | 'medico' | 'admin' | 'default', explicit?: string, redirectBase?: string) {
const DEFAULT_REDIRECT_BASE = redirectBase ?? 'https://mediconecta-app-liart.vercel.app';
if (explicit) {
// If explicit is already absolute, return trimmed
try {
const u = new URL(explicit);
return u.toString().replace(/\/$/, '');
} catch (e) {
// Not an absolute URL, fall through to build from base
}
}
const base = DEFAULT_REDIRECT_BASE.replace(/\/$/, '');
let path = '/';
if (target === 'paciente') path = '/paciente';
else if (target === 'medico') path = '/profissional';
else if (target === 'admin') path = '/dashboard';
return `${base}${path}`;
}
// Token salvo no browser (aceita auth_token ou token) // Token salvo no browser (aceita auth_token ou token)
function getAuthToken(): string | null { function getAuthToken(): string | null {
if (typeof window === "undefined") return null; if (typeof window === "undefined") return null;
@ -1587,6 +1608,13 @@ export type CreateUserInput = {
full_name: string; full_name: string;
phone?: string | null; phone?: string | null;
role: UserRoleEnum; role: UserRoleEnum;
// Optional: when provided, backend can use this to send magic links that redirect
// to the given URL or interpret `target` to build a role-specific redirect.
emailRedirectTo?: string;
// Compatibility: some integrations expect `redirect_url` as the parameter name
// for the post-auth redirect. Include it so backend/functions receive it.
redirect_url?: string;
target?: 'paciente' | 'medico' | 'admin' | 'default';
}; };
export type CreatedUser = { export type CreatedUser = {
@ -1767,28 +1795,35 @@ export async function criarUsuarioMedico(medico: { email: string; full_name: str
// Rely on the server-side create-user endpoint (POST /create-user). The // Rely on the server-side create-user endpoint (POST /create-user). The
// backend is responsible for role assignment and sending the magic link. // backend is responsible for role assignment and sending the magic link.
// Any error should be surfaced to the caller so it can be handled there. // Any error should be surfaced to the caller so it can be handled there.
return await criarUsuario({ email: medico.email, password: '', full_name: medico.full_name, phone: medico.phone_mobile, role: 'medico' as any }); const redirectBase = 'https://mediconecta-app-liart.vercel.app';
const emailRedirectTo = `${redirectBase.replace(/\/$/, '')}/profissional`;
const redirect_url = emailRedirectTo;
return await criarUsuario({ email: medico.email, password: '', full_name: medico.full_name, phone: medico.phone_mobile, role: 'medico' as any, emailRedirectTo, redirect_url, target: 'medico' });
} }
// Criar usuário para PACIENTE no Supabase Auth (sistema de autenticação) // Criar usuário para PACIENTE no Supabase Auth (sistema de autenticação)
export async function criarUsuarioPaciente(paciente: { email: string; full_name: string; phone_mobile: string; }): Promise<any> { export async function criarUsuarioPaciente(paciente: { email: string; full_name: string; phone_mobile: string; }): Promise<any> {
// Rely on the server-side create-user endpoint (POST /create-user). // Rely on the server-side create-user endpoint (POST /create-user).
return await criarUsuario({ email: paciente.email, password: '', full_name: paciente.full_name, phone: paciente.phone_mobile, role: 'paciente' as any }); const redirectBase = 'https://mediconecta-app-liart.vercel.app';
const emailRedirectTo = `${redirectBase.replace(/\/$/, '')}/paciente`;
const redirect_url = emailRedirectTo;
return await criarUsuario({ email: paciente.email, password: '', full_name: paciente.full_name, phone: paciente.phone_mobile, role: 'paciente' as any, emailRedirectTo, redirect_url, target: 'paciente' });
} }
/**
* Envia um magic link (OTP) diretamente via Supabase Auth (cliente) export async function sendMagicLink(
* Sem componente server-side adicional. Use quando quiser autenticar email: string,
* o usuário por email (senha não necessária). options?: { emailRedirectTo?: string; target?: 'paciente' | 'medico' | 'admin' | 'default'; redirectBase?: string }
* ): Promise<{ success: boolean; message?: string }> {
* Observação: isto apenas envia o link de login. A atribuição de roles
* continua sendo operação server-side e deve ser feita pelo backend.
*/
export async function sendMagicLink(email: string, options?: { emailRedirectTo?: string }): Promise<{ success: boolean; message?: string }> {
if (!email) throw new Error('Email obrigatório para enviar magic link'); if (!email) throw new Error('Email obrigatório para enviar magic link');
const url = `${API_BASE}/auth/v1/otp`; const url = `${API_BASE}/auth/v1/otp`;
const payload: any = { email }; const payload: any = { email };
if (options && options.emailRedirectTo) payload.options = { emailRedirectTo: options.emailRedirectTo };
const redirectUrl = buildRedirectUrl(options?.target, options?.emailRedirectTo, options?.redirectBase);
if (redirectUrl) {
// include both keys for broader compatibility across different Supabase setups
payload.options = { emailRedirectTo: redirectUrl, redirect_to: redirectUrl, redirect_url: redirectUrl };
}
try { try {
const res = await fetch(url, { const res = await fetch(url, {