mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-05 05:05:08 +02:00
fix: resolve team_id during auth and preserve across token refresh
P1 from Codex review: interactive auth saved team_id: '' making all subsequent sync operations fail. Now resolves team_id from team_members table immediately after OAuth callback. Also fixes token refresh in sync.ts to preserve the existing team_id instead of resetting it to empty, and removes order=created_at.desc from pullTable() default query since sync_heartbeats and team_members tables don't have that column (P2). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+37
-3
@@ -32,7 +32,7 @@ export async function runDeviceAuth(team: TeamConfig): Promise<AuthTokens> {
|
||||
reject(new Error('Auth timed out after 5 minutes. Please try again.'));
|
||||
}, AUTH_TIMEOUT_MS);
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url || '/', `http://localhost:${AUTH_CALLBACK_PORT}`);
|
||||
|
||||
// Handle the OAuth callback
|
||||
@@ -53,7 +53,7 @@ export async function runDeviceAuth(team: TeamConfig): Promise<AuthTokens> {
|
||||
refresh_token: refreshToken,
|
||||
expires_at: Math.floor(Date.now() / 1000) + expiresIn,
|
||||
user_id: url.searchParams.get('user_id') || '',
|
||||
team_id: '', // filled in by sync.ts after first API call
|
||||
team_id: '',
|
||||
email: url.searchParams.get('email') || '',
|
||||
};
|
||||
|
||||
@@ -63,6 +63,12 @@ export async function runDeviceAuth(team: TeamConfig): Promise<AuthTokens> {
|
||||
clearTimeout(timeout);
|
||||
server.close();
|
||||
|
||||
// Resolve team_id from team_members table before saving
|
||||
try {
|
||||
const teamId = await resolveTeamId(team, tokens.access_token, tokens.user_id);
|
||||
if (teamId) tokens.team_id = teamId;
|
||||
} catch { /* non-fatal — team_id can be resolved later */ }
|
||||
|
||||
// Save tokens
|
||||
try {
|
||||
saveAuthTokens(team.supabase_url, tokens);
|
||||
@@ -79,7 +85,7 @@ export async function runDeviceAuth(team: TeamConfig): Promise<AuthTokens> {
|
||||
if (url.pathname === '/auth/token' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
|
||||
req.on('end', () => {
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
const tokens: AuthTokens = {
|
||||
@@ -97,6 +103,12 @@ export async function runDeviceAuth(team: TeamConfig): Promise<AuthTokens> {
|
||||
clearTimeout(timeout);
|
||||
server.close();
|
||||
|
||||
// Resolve team_id before saving
|
||||
try {
|
||||
const teamId = await resolveTeamId(team, tokens.access_token, tokens.user_id);
|
||||
if (teamId) tokens.team_id = teamId;
|
||||
} catch { /* non-fatal */ }
|
||||
|
||||
saveAuthTokens(team.supabase_url, tokens);
|
||||
resolve(tokens);
|
||||
} catch (err: any) {
|
||||
@@ -209,3 +221,25 @@ export function isTokenExpired(tokens: AuthTokens): boolean {
|
||||
const buffer = 300; // 5-minute buffer
|
||||
return Math.floor(Date.now() / 1000) >= tokens.expires_at - buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up the user's team_id from team_members table after auth.
|
||||
* Returns the first team_id found, or null if the lookup fails.
|
||||
*/
|
||||
async function resolveTeamId(team: TeamConfig, accessToken: string, userId: string): Promise<string | null> {
|
||||
if (!userId) return null;
|
||||
try {
|
||||
const url = `${team.supabase_url}/rest/v1/team_members?user_id=eq.${userId}&select=team_id&limit=1`;
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
'apikey': team.supabase_anon_key,
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const rows = await res.json() as Array<{ team_id: string }>;
|
||||
return rows.length > 0 ? rows[0].team_id : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -39,7 +39,7 @@ interface CacheMeta {
|
||||
* Refresh an expired access token using the refresh token.
|
||||
* Returns new tokens on success, null on failure.
|
||||
*/
|
||||
async function refreshToken(supabaseUrl: string, refreshToken: string, anonKey: string): Promise<AuthTokens | null> {
|
||||
async function refreshToken(supabaseUrl: string, refreshToken: string, anonKey: string, existingTeamId?: string): Promise<AuthTokens | null> {
|
||||
try {
|
||||
const res = await fetchWithTimeout(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
|
||||
method: 'POST',
|
||||
@@ -58,7 +58,7 @@ async function refreshToken(supabaseUrl: string, refreshToken: string, anonKey:
|
||||
refresh_token: data.refresh_token as string || refreshToken,
|
||||
expires_at: Math.floor(Date.now() / 1000) + ((data.expires_in as number) || 3600),
|
||||
user_id: (data.user as any)?.id || '',
|
||||
team_id: '',
|
||||
team_id: existingTeamId || '', // preserve existing team_id across refresh
|
||||
email: (data.user as any)?.email || '',
|
||||
};
|
||||
} catch {
|
||||
@@ -78,6 +78,7 @@ export async function getValidToken(config: SyncConfig): Promise<string | null>
|
||||
config.team.supabase_url,
|
||||
config.auth.refresh_token,
|
||||
config.team.supabase_anon_key,
|
||||
config.auth.team_id,
|
||||
);
|
||||
|
||||
if (!newTokens) return null;
|
||||
@@ -234,7 +235,7 @@ export async function pullTable(table: string, query?: string): Promise<Record<s
|
||||
|
||||
const url = query
|
||||
? `${restUrl(config.team.supabase_url, table)}?${query}`
|
||||
: `${restUrl(config.team.supabase_url, table)}?team_id=eq.${config.auth.team_id}&order=created_at.desc&limit=500`;
|
||||
: `${restUrl(config.team.supabase_url, table)}?team_id=eq.${config.auth.team_id}&limit=500`;
|
||||
|
||||
const res = await fetchWithTimeout(url, {
|
||||
method: 'GET',
|
||||
|
||||
Reference in New Issue
Block a user