juntekim.com/stripe_to_invoice/lib/xero/auth.ts

66 lines
1.7 KiB
TypeScript

import "server-only";
import { db } from "@/lib/db";
import { xeroConnections } from "@/lib/schema/xeroConnections";
import { eq } from "drizzle-orm";
/**
* Returns a valid Xero access token for the given user.
* Refreshes and persists tokens automatically if expired.
*/
export async function getValidXeroAccessToken(userId: string): Promise<string> {
const conn = await db.query.xeroConnections.findFirst({
where: (t, { eq }) => eq(t.userId, userId),
});
if (!conn) {
throw new Error("No Xero connection");
}
const now = Date.now();
// Access token still valid (60s safety buffer)
if (now < conn.expiresAt.getTime() - 60_000) {
return conn.accessToken;
}
// Refresh token
const res = 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: "refresh_token",
refresh_token: conn.refreshToken,
}),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Failed to refresh Xero token: ${text}`);
}
const tokens: {
access_token: string;
refresh_token: string;
expires_in: number;
} = await res.json();
await db
.update(xeroConnections)
.set({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token, // 🔥 must overwrite
expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
updatedAt: new Date(),
})
.where(eq(xeroConnections.id, conn.id));
return tokens.access_token;
}