fix: guardar OAuth Google con upsert real (PATCH vacío no insertaba)

El callback creía haber guardado el token y fallaba al emparejar Drive
con «no conectado». Upsert on_conflict=provider + match no bloquea OAuth.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-24 12:17:46 +02:00
parent 5d04a79de7
commit 5501f45a67
3 changed files with 59 additions and 46 deletions

View File

@@ -56,10 +56,11 @@ export async function GET(req: NextRequest) {
email, email,
}); });
// Auto-match // Auto-match folders (non-fatal: tokens already saved)
const headers = serviceHeaders();
let linked = 0; let linked = 0;
let ambiguous = 0; let ambiguous = 0;
try {
const headers = serviceHeaders();
if (headers) { if (headers) {
const tRes = await fetch( const tRes = await fetch(
`${supabaseUrl()}/rest/v1/tenants?select=id,name,slug&order=name.asc`, `${supabaseUrl()}/rest/v1/tenants?select=id,name,slug&order=name.asc`,
@@ -78,6 +79,9 @@ export async function GET(req: NextRequest) {
ambiguous = matches.filter((m) => m.status === 'ambiguous').length; ambiguous = matches.filter((m) => m.status === 'ambiguous').length;
} }
} }
} catch {
// match can fail without undoing OAuth
}
const res = NextResponse.redirect( const res = NextResponse.redirect(
`${appUrl()}/dashboard/agencia?drive=ok&linked=${linked}&ambiguous=${ambiguous}` `${appUrl()}/dashboard/agencia?drive=ok&linked=${linked}&ambiguous=${ambiguous}`

View File

@@ -38,14 +38,17 @@ export default function DriveConnectPanel() {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
if (params.get('drive') === 'ok') { if (params.get('drive') === 'ok') {
setMsg( setMsg(
`Drive conectado. Enlazadas ${params.get('linked') || 0} carpetas` + `Google conectado. Enlazadas ${params.get('linked') || 0} carpetas Drive` +
(params.get('ambiguous') && params.get('ambiguous') !== '0' (params.get('ambiguous') && params.get('ambiguous') !== '0'
? ` · ${params.get('ambiguous')} ambiguas (revisa abajo)` ? ` · ${params.get('ambiguous')} ambiguas (revisa abajo)`
: '') : '')
); );
} }
if (params.get('drive') === 'error') { if (params.get('drive') === 'error') {
setErr(params.get('msg') || 'Error OAuth Drive'); setErr(params.get('msg') || 'Error OAuth Google');
}
if (params.get('drive')) {
window.history.replaceState({}, '', '/dashboard/agencia');
} }
void loadStatus(); void loadStatus();
}, []); }, []);
@@ -133,13 +136,21 @@ export default function DriveConnectPanel() {
</div> </div>
</div> </div>
{connected && ( {connected ? (
<p className="text-xs text-emerald-400"> <p className="text-xs text-emerald-700">
Conectado{email ? ` · ${email}` : ''} Conectado{email ? ` · ${email}` : ''}
</p> </p>
) : (
<p className="text-xs text-amber-800">
Aún no conectado — pulsa el botón naranja.
</p>
)} )}
{msg && <p className="text-xs text-gray-600">{msg}</p>} {msg && <p className="text-xs text-gray-600">{msg}</p>}
{err && <p className="text-xs text-red-400">{err}</p>} {err && (
<p className="text-xs text-red-700 bg-red-50 border border-red-200 rounded-lg px-3 py-2">
{err}
</p>
)}
{matches && matches.length > 0 && ( {matches && matches.length > 0 && (
<ul className="space-y-2 max-h-72 overflow-y-auto"> <ul className="space-y-2 max-h-72 overflow-y-auto">

View File

@@ -133,29 +133,27 @@ export async function saveAgencyOAuth(opts: {
}; };
if (opts.refreshToken) row.refresh_token = opts.refreshToken; if (opts.refreshToken) row.refresh_token = opts.refreshToken;
// Upsert: try PATCH then POST // PostgREST PATCH returns 204 even when 0 rows match — do not treat that as success.
const base = `${supabaseUrl()}/rest/v1/agency_oauth?provider=eq.${DRIVE_PROVIDER}`; // Upsert on primary key `provider`.
const patch = await fetch(base, { const upsert = await fetch(
method: 'PATCH', `${supabaseUrl()}/rest/v1/agency_oauth?on_conflict=provider`,
headers: { {
...headers,
'Content-Type': 'application/json',
Prefer: 'return=minimal',
},
body: JSON.stringify(row),
});
if (patch.ok) return;
const ins = await fetch(`${supabaseUrl()}/rest/v1/agency_oauth`, {
method: 'POST', method: 'POST',
headers: { headers: {
...headers, ...headers,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Prefer: 'resolution=merge-duplicates', Prefer: 'resolution=merge-duplicates,return=representation',
}, },
body: JSON.stringify(row), body: JSON.stringify(row),
}); }
if (!ins.ok) throw new Error(`save oauth: ${(await ins.text()).slice(0, 300)}`); );
if (!upsert.ok) {
throw new Error(`save oauth: ${(await upsert.text()).slice(0, 300)}`);
}
const saved = (await upsert.json()) as unknown[];
if (!Array.isArray(saved) || saved.length === 0) {
throw new Error('save oauth: upsert no escribió filas');
}
} }
type OAuthRow = { type OAuthRow = {