juntekim.com/stripe_to_invoice/app/api/xero/callback/route.ts
2026-01-21 19:44:38 +00:00

114 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { xeroConnections } from "@/lib/schema/xeroConnections";
import { eq } from "drizzle-orm";
type XeroTokenResponse = {
access_token: string;
refresh_token: string;
expires_in: number; // seconds
};
type XeroTenant = {
tenantId: string;
};
export async function GET(req: NextRequest) {
const cookieStore = await cookies();
const session = cookieStore.get("session");
if (!session) {
return NextResponse.redirect(
new URL("/login", process.env.APP_URL)
);
}
const userId = session.value;
const { searchParams } = new URL(req.url);
const code = searchParams.get("code");
if (!code) {
return NextResponse.json(
{ error: "Missing OAuth code" },
{ status: 400 }
);
}
// 1⃣ Exchange code for tokens
const tokenRes = await fetch("https://identity.xero.com/connect/token", {
method: "POST",
headers: {
Authorization:
"Basic " +
Buffer.from(
`${process.env.XERO_CLIENT_ID}:${process.env.XERO_CLIENT_SECRET}`
).toString("base64"),
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: process.env.XERO_REDIRECT_URI!,
}),
});
if (!tokenRes.ok) {
const text = await tokenRes.text();
console.error("Xero token exchange failed:", text);
return NextResponse.redirect(
new URL("/connect/xero?error=token_failed", process.env.APP_URL)
);
}
const tokenData = (await tokenRes.json()) as XeroTokenResponse;
// 2⃣ Fetch tenant
const tenantRes = await fetch("https://api.xero.com/connections", {
headers: {
Authorization: `Bearer ${tokenData.access_token}`,
},
});
const tenants = (await tenantRes.json()) as XeroTenant[];
const tenantId = tenants[0]?.tenantId;
if (!tenantId) {
return NextResponse.json(
{ error: "No Xero organisation found" },
{ status: 400 }
);
}
const expiresAt = new Date(Date.now() + tokenData.expires_in * 1000);
// 3⃣ Persist EVERYTHING (this is the fix)
await db
.insert(xeroConnections)
.values({
userId,
tenantId,
accessToken: tokenData.access_token,
refreshToken: tokenData.refresh_token,
expiresAt,
salesAccountCode: "200",
stripeClearingAccountCode: "610",
updatedAt: new Date(),
})
.onConflictDoUpdate({
target: xeroConnections.userId,
set: {
tenantId,
accessToken: tokenData.access_token,
refreshToken: tokenData.refresh_token,
expiresAt,
updatedAt: new Date(),
// ⚠️ deliberately NOT updating account codes
},
});
return NextResponse.redirect(
new URL("/connect/xero/success", process.env.APP_URL)
);
}