66 lines
1.7 KiB
TypeScript
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;
|
|
}
|