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,
});
// Auto-match
const headers = serviceHeaders();
// Auto-match folders (non-fatal: tokens already saved)
let linked = 0;
let ambiguous = 0;
try {
const headers = serviceHeaders();
if (headers) {
const tRes = await fetch(
`${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;
}
}
} catch {
// match can fail without undoing OAuth
}
const res = NextResponse.redirect(
`${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);
if (params.get('drive') === 'ok') {
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')} ambiguas (revisa abajo)`
: '')
);
}
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();
}, []);
@@ -133,13 +136,21 @@ export default function DriveConnectPanel() {
</div>
</div>
{connected && (
<p className="text-xs text-emerald-400">
{connected ? (
<p className="text-xs text-emerald-700">
Conectado{email ? ` · ${email}` : ''}
</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>}
{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 && (
<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;
// Upsert: try PATCH then POST
const base = `${supabaseUrl()}/rest/v1/agency_oauth?provider=eq.${DRIVE_PROVIDER}`;
const patch = await fetch(base, {
method: 'PATCH',
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`, {
// PostgREST PATCH returns 204 even when 0 rows match — do not treat that as success.
// Upsert on primary key `provider`.
const upsert = await fetch(
`${supabaseUrl()}/rest/v1/agency_oauth?on_conflict=provider`,
{
method: 'POST',
headers: {
...headers,
'Content-Type': 'application/json',
Prefer: 'resolution=merge-duplicates',
Prefer: 'resolution=merge-duplicates,return=representation',
},
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 = {