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,27 +56,31 @@ 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;
if (headers) { try {
const tRes = await fetch( const headers = serviceHeaders();
`${supabaseUrl()}/rest/v1/tenants?select=id,name,slug&order=name.asc`, if (headers) {
{ headers, cache: 'no-store' } const tRes = await fetch(
); `${supabaseUrl()}/rest/v1/tenants?select=id,name,slug&order=name.asc`,
if (tRes.ok) { { headers, cache: 'no-store' }
const tenants = (await tRes.json()) as { );
id: string; if (tRes.ok) {
name: string; const tenants = (await tRes.json()) as {
slug: string; id: string;
}[]; name: string;
const folders = await listDriveFolders(); slug: string;
const matches = await matchFoldersToTenants(tenants, folders); }[];
const applied = await applyMatches(matches); const folders = await listDriveFolders();
linked = applied.linked; const matches = await matchFoldersToTenants(tenants, folders);
ambiguous = matches.filter((m) => m.status === 'ambiguous').length; const applied = await applyMatches(matches);
linked = applied.linked;
ambiguous = matches.filter((m) => m.status === 'ambiguous').length;
}
} }
} catch {
// match can fail without undoing OAuth
} }
const res = NextResponse.redirect( const res = NextResponse.redirect(

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, method: 'POST',
'Content-Type': 'application/json', headers: {
Prefer: 'return=minimal', ...headers,
}, 'Content-Type': 'application/json',
body: JSON.stringify(row), Prefer: 'resolution=merge-duplicates,return=representation',
}); },
if (patch.ok) return; body: JSON.stringify(row),
}
const ins = await fetch(`${supabaseUrl()}/rest/v1/agency_oauth`, { );
method: 'POST', if (!upsert.ok) {
headers: { throw new Error(`save oauth: ${(await upsert.text()).slice(0, 300)}`);
...headers, }
'Content-Type': 'application/json', const saved = (await upsert.json()) as unknown[];
Prefer: 'resolution=merge-duplicates', if (!Array.isArray(saved) || saved.length === 0) {
}, throw new Error('save oauth: upsert no escribió filas');
body: JSON.stringify(row), }
});
if (!ins.ok) throw new Error(`save oauth: ${(await ins.text()).slice(0, 300)}`);
} }
type OAuthRow = { type OAuthRow = {