Handle token refresh for supabase
This commit is contained in:
@@ -1,12 +1,11 @@
|
|||||||
// Track app operations that are in progress
|
const locks = new Map<number | string, Promise<void>>();
|
||||||
const appOperationLocks = new Map<number, Promise<void>>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Acquires a lock for an app operation
|
* Acquires a lock for an app operation
|
||||||
* @param appId The app ID to lock
|
* @param lockId The app ID to lock
|
||||||
* @returns An object with release function and promise
|
* @returns An object with release function and promise
|
||||||
*/
|
*/
|
||||||
export function acquireLock(appId: number): {
|
export function acquireLock(lockId: number | string): {
|
||||||
release: () => void;
|
release: () => void;
|
||||||
promise: Promise<void>;
|
promise: Promise<void>;
|
||||||
} {
|
} {
|
||||||
@@ -14,33 +13,33 @@ export function acquireLock(appId: number): {
|
|||||||
|
|
||||||
const promise = new Promise<void>((resolve) => {
|
const promise = new Promise<void>((resolve) => {
|
||||||
release = () => {
|
release = () => {
|
||||||
appOperationLocks.delete(appId);
|
locks.delete(lockId);
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
appOperationLocks.set(appId, promise);
|
locks.set(lockId, promise);
|
||||||
return { release, promise };
|
return { release, promise };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a function with a lock on the app ID
|
* Executes a function with a lock on the lock ID
|
||||||
* @param appId The app ID to lock
|
* @param lockId The lock ID to lock
|
||||||
* @param fn The function to execute with the lock
|
* @param fn The function to execute with the lock
|
||||||
* @returns Result of the function
|
* @returns Result of the function
|
||||||
*/
|
*/
|
||||||
export async function withLock<T>(
|
export async function withLock<T>(
|
||||||
appId: number,
|
lockId: number | string,
|
||||||
fn: () => Promise<T>
|
fn: () => Promise<T>
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
// Wait for any existing operation to complete
|
// Wait for any existing operation to complete
|
||||||
const existingLock = appOperationLocks.get(appId);
|
const existingLock = locks.get(lockId);
|
||||||
if (existingLock) {
|
if (existingLock) {
|
||||||
await existingLock;
|
await existingLock;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquire a new lock
|
// Acquire a new lock
|
||||||
const { release, promise } = acquireLock(appId);
|
const { release, promise } = acquireLock(lockId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await fn();
|
const result = await fn();
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export const SupabaseSchema = z.object({
|
|||||||
accessToken: SecretSchema.optional(),
|
accessToken: SecretSchema.optional(),
|
||||||
refreshToken: SecretSchema.optional(),
|
refreshToken: SecretSchema.optional(),
|
||||||
expiresIn: z.number().optional(),
|
expiresIn: z.number().optional(),
|
||||||
|
tokenTimestamp: z.number().optional(),
|
||||||
});
|
});
|
||||||
export type Supabase = z.infer<typeof SupabaseSchema>;
|
export type Supabase = z.infer<typeof SupabaseSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,90 @@
|
|||||||
import { readSettings } from "../main/settings";
|
import { withLock } from "../ipc/utils/lock_utils";
|
||||||
|
import { readSettings, writeSettings } from "../main/settings";
|
||||||
import { SupabaseManagementAPI } from "supabase-management-js";
|
import { SupabaseManagementAPI } from "supabase-management-js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the Supabase access token is expired or about to expire
|
||||||
|
* Returns true if token needs to be refreshed
|
||||||
|
*/
|
||||||
|
function isTokenExpired(expiresIn?: number): boolean {
|
||||||
|
if (!expiresIn) return true;
|
||||||
|
|
||||||
|
// Get when the token was saved (expiresIn is stored at the time of token receipt)
|
||||||
|
const settings = readSettings();
|
||||||
|
const tokenTimestamp = settings.supabase?.tokenTimestamp || 0;
|
||||||
|
const currentTime = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
// Check if the token is expired or about to expire (within 5 minutes)
|
||||||
|
return currentTime >= tokenTimestamp + expiresIn - 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the Supabase access token using the refresh token
|
||||||
|
* Updates settings with new tokens and expiration time
|
||||||
|
*/
|
||||||
|
export async function refreshSupabaseToken(): Promise<void> {
|
||||||
|
const settings = readSettings();
|
||||||
|
const refreshToken = settings.supabase?.refreshToken?.value;
|
||||||
|
|
||||||
|
if (!isTokenExpired(settings.supabase?.expiresIn)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new Error(
|
||||||
|
"Supabase refresh token not found. Please authenticate first."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Make request to Supabase refresh endpoint
|
||||||
|
const response = await fetch(
|
||||||
|
"https://supabase-oauth.dyad.sh/api/connect-supabase/refresh",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ refreshToken }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Token refresh failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
accessToken,
|
||||||
|
refreshToken: newRefreshToken,
|
||||||
|
expiresIn,
|
||||||
|
} = await response.json();
|
||||||
|
|
||||||
|
// Update settings with new tokens
|
||||||
|
writeSettings({
|
||||||
|
supabase: {
|
||||||
|
accessToken: {
|
||||||
|
value: accessToken,
|
||||||
|
},
|
||||||
|
refreshToken: {
|
||||||
|
value: newRefreshToken,
|
||||||
|
},
|
||||||
|
expiresIn,
|
||||||
|
tokenTimestamp: Math.floor(Date.now() / 1000), // Store current timestamp
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refreshing Supabase token:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Function to get the Supabase Management API client
|
// Function to get the Supabase Management API client
|
||||||
export async function getSupabaseClient(): Promise<SupabaseManagementAPI> {
|
export async function getSupabaseClient(): Promise<SupabaseManagementAPI> {
|
||||||
const settings = readSettings();
|
const settings = readSettings();
|
||||||
|
|
||||||
// Check if Supabase token exists in settings
|
// Check if Supabase token exists in settings
|
||||||
const supabaseAccessToken = settings.supabase?.accessToken?.value;
|
const supabaseAccessToken = settings.supabase?.accessToken?.value;
|
||||||
|
const expiresIn = settings.supabase?.expiresIn;
|
||||||
|
|
||||||
if (!supabaseAccessToken) {
|
if (!supabaseAccessToken) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -13,6 +92,22 @@ export async function getSupabaseClient(): Promise<SupabaseManagementAPI> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if token needs refreshing
|
||||||
|
if (isTokenExpired(expiresIn)) {
|
||||||
|
await withLock("refresh-supabase-token", refreshSupabaseToken);
|
||||||
|
// Get updated settings after refresh
|
||||||
|
const updatedSettings = readSettings();
|
||||||
|
const newAccessToken = updatedSettings.supabase?.accessToken?.value;
|
||||||
|
|
||||||
|
if (!newAccessToken) {
|
||||||
|
throw new Error("Failed to refresh Supabase access token");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SupabaseManagementAPI({
|
||||||
|
accessToken: newAccessToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return new SupabaseManagementAPI({
|
return new SupabaseManagementAPI({
|
||||||
accessToken: supabaseAccessToken,
|
accessToken: supabaseAccessToken,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export function handleSupabaseOAuthReturn({
|
|||||||
value: refreshToken,
|
value: refreshToken,
|
||||||
},
|
},
|
||||||
expiresIn,
|
expiresIn,
|
||||||
|
tokenTimestamp: Math.floor(Date.now() / 1000),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user