114 lines
2.9 KiB
TypeScript
114 lines
2.9 KiB
TypeScript
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)
|
||
);
|
||
}
|