Spaces:
Sleeping
Sleeping
Commit ·
78e893f
1
Parent(s): e19cdf4
apply changes
Browse files- src/app/api/auth/github/callback/route.ts +3 -1
- src/app/api/sync/run/route.ts +34 -18
- src/app/api/sync/status/route.ts +45 -19
- src/db/schema.ts +4 -5
- src/lib/auth.ts +3 -1
src/app/api/auth/github/callback/route.ts
CHANGED
|
@@ -57,7 +57,6 @@ export async function GET(request: NextRequest) {
|
|
| 57 |
const githubUser = await userResponse.json();
|
| 58 |
|
| 59 |
// Check if user exists
|
| 60 |
-
// TODO: After Turso migration, add syncStatus, lastSyncAt, syncError to this select
|
| 61 |
const existingUsers = await db
|
| 62 |
.select({
|
| 63 |
id: users.id,
|
|
@@ -66,6 +65,9 @@ export async function GET(request: NextRequest) {
|
|
| 66 |
avatarUrl: users.avatarUrl,
|
| 67 |
role: users.role,
|
| 68 |
githubAccessToken: users.githubAccessToken,
|
|
|
|
|
|
|
|
|
|
| 69 |
createdAt: users.createdAt,
|
| 70 |
updatedAt: users.updatedAt,
|
| 71 |
})
|
|
|
|
| 57 |
const githubUser = await userResponse.json();
|
| 58 |
|
| 59 |
// Check if user exists
|
|
|
|
| 60 |
const existingUsers = await db
|
| 61 |
.select({
|
| 62 |
id: users.id,
|
|
|
|
| 65 |
avatarUrl: users.avatarUrl,
|
| 66 |
role: users.role,
|
| 67 |
githubAccessToken: users.githubAccessToken,
|
| 68 |
+
syncStatus: users.syncStatus,
|
| 69 |
+
lastSyncAt: users.lastSyncAt,
|
| 70 |
+
syncError: users.syncError,
|
| 71 |
createdAt: users.createdAt,
|
| 72 |
updatedAt: users.updatedAt,
|
| 73 |
})
|
src/app/api/sync/run/route.ts
CHANGED
|
@@ -7,10 +7,16 @@
|
|
| 7 |
|
| 8 |
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
-
import { runFullSync, runMaintainerSync, runContributorSync, reconcileOpenTriageIssue1, syncContributorPRsDirect, SYNC_INTERVAL_MS } from "@/lib/sync/github-sync";
|
| 11 |
import { db } from "@/db";
|
| 12 |
import { users } from "@/db/schema";
|
| 13 |
import { eq } from "drizzle-orm";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
export async function POST(request: NextRequest) {
|
| 16 |
try {
|
|
@@ -23,11 +29,22 @@ export async function POST(request: NextRequest) {
|
|
| 23 |
return NextResponse.json({ error: "GitHub access token not found" }, { status: 400 });
|
| 24 |
}
|
| 25 |
|
| 26 |
-
//
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
.from(users)
|
| 32 |
.where(eq(users.id, user.id))
|
| 33 |
.limit(1);
|
|
@@ -40,7 +57,7 @@ export async function POST(request: NextRequest) {
|
|
| 40 |
|
| 41 |
// Circuit breaker: Check if sync already in progress
|
| 42 |
if (currentSyncStatus === 'SYNCING') {
|
| 43 |
-
console.log(`[SyncRun] User ${user.id} sync already in progress`);
|
| 44 |
return NextResponse.json({
|
| 45 |
error: "Sync already in progress",
|
| 46 |
status: "SYNCING",
|
|
@@ -57,14 +74,17 @@ export async function POST(request: NextRequest) {
|
|
| 57 |
}, { status: 202 });
|
| 58 |
}
|
| 59 |
|
| 60 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
await db.update(users)
|
| 62 |
.set({
|
| 63 |
syncStatus: 'SYNCING',
|
| 64 |
syncError: null
|
| 65 |
})
|
| 66 |
.where(eq(users.id, user.id));
|
| 67 |
-
*/
|
| 68 |
|
| 69 |
console.log(`[SyncRun] Starting sync for user ${user.id}`);
|
| 70 |
|
|
@@ -89,15 +109,12 @@ export async function POST(request: NextRequest) {
|
|
| 89 |
// Always reconcile the critical openTriage#1 issue to ensure immediate state sync
|
| 90 |
const reconcileResult = await reconcileOpenTriageIssue1(user.githubAccessToken);
|
| 91 |
|
| 92 |
-
// TODO: Uncomment after Turso migration
|
| 93 |
-
/*
|
| 94 |
// Mark sync as COMPLETED
|
| 95 |
await db.update(users).set({
|
| 96 |
syncStatus: 'COMPLETED',
|
| 97 |
lastSyncAt: new Date().toISOString(),
|
| 98 |
syncError: null
|
| 99 |
}).where(eq(users.id, user.id));
|
| 100 |
-
*/
|
| 101 |
|
| 102 |
console.log(`[SyncRun] Sync completed for user ${user.id}`);
|
| 103 |
|
|
@@ -116,21 +133,20 @@ export async function POST(request: NextRequest) {
|
|
| 116 |
} catch (syncError: any) {
|
| 117 |
console.error(`[SyncRun] Error syncing for user ${user.id}:`, syncError);
|
| 118 |
|
| 119 |
-
//
|
| 120 |
-
/*
|
| 121 |
-
// Mark sync as FAILED
|
| 122 |
await db.update(users).set({
|
| 123 |
syncStatus: 'FAILED',
|
| 124 |
syncError: syncError.message || 'Unknown error'
|
| 125 |
}).where(eq(users.id, user.id));
|
| 126 |
-
*/
|
| 127 |
-
|
| 128 |
-
console.error(`[SyncRun] Sync failed for user ${user.id}:`, syncError);
|
| 129 |
|
| 130 |
return NextResponse.json({
|
| 131 |
error: "Sync failed",
|
| 132 |
message: syncError.message || "Internal server error"
|
| 133 |
}, { status: 500 });
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
}
|
| 135 |
} catch (error) {
|
| 136 |
console.error("POST /api/sync/run error:", error);
|
|
|
|
| 7 |
|
| 8 |
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
import { getCurrentUser } from "@/lib/auth";
|
|
|
|
| 10 |
import { db } from "@/db";
|
| 11 |
import { users } from "@/db/schema";
|
| 12 |
import { eq } from "drizzle-orm";
|
| 13 |
+
import { runMaintainerSync, runContributorSync, syncContributorPRsDirect, reconcileOpenTriageIssue1 } from "@/lib/sync/github-sync";
|
| 14 |
+
|
| 15 |
+
const SYNC_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
| 16 |
+
|
| 17 |
+
// Layer 2 Defense: In-memory set to prevent race conditions
|
| 18 |
+
// This provides instant protection before database queries
|
| 19 |
+
const activeSyncs = new Set<string>();
|
| 20 |
|
| 21 |
export async function POST(request: NextRequest) {
|
| 22 |
try {
|
|
|
|
| 29 |
return NextResponse.json({ error: "GitHub access token not found" }, { status: 400 });
|
| 30 |
}
|
| 31 |
|
| 32 |
+
// Layer 2 Defense: In-memory check (FAST - prevents race conditions)
|
| 33 |
+
if (activeSyncs.has(user.id)) {
|
| 34 |
+
console.log(`[SyncRun] User ${user.id} sync blocked by in-memory guard`);
|
| 35 |
+
return NextResponse.json({
|
| 36 |
+
error: "Sync already in progress",
|
| 37 |
+
status: "SYNCING",
|
| 38 |
+
message: "Please wait for the current sync to complete"
|
| 39 |
+
}, { status: 429 });
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Layer 1 Defense: Database check (PERSISTENT - survives restarts)
|
| 43 |
+
const userRecord = await db.select({
|
| 44 |
+
id: users.id,
|
| 45 |
+
syncStatus: users.syncStatus,
|
| 46 |
+
lastSyncAt: users.lastSyncAt,
|
| 47 |
+
})
|
| 48 |
.from(users)
|
| 49 |
.where(eq(users.id, user.id))
|
| 50 |
.limit(1);
|
|
|
|
| 57 |
|
| 58 |
// Circuit breaker: Check if sync already in progress
|
| 59 |
if (currentSyncStatus === 'SYNCING') {
|
| 60 |
+
console.log(`[SyncRun] User ${user.id} sync already in progress (DB status)`);
|
| 61 |
return NextResponse.json({
|
| 62 |
error: "Sync already in progress",
|
| 63 |
status: "SYNCING",
|
|
|
|
| 74 |
}, { status: 202 });
|
| 75 |
}
|
| 76 |
|
| 77 |
+
// Add to in-memory set FIRST (before DB update)
|
| 78 |
+
activeSyncs.add(user.id);
|
| 79 |
+
console.log(`[SyncRun] Added user ${user.id} to activeSyncs. Size: ${activeSyncs.size}`);
|
| 80 |
+
|
| 81 |
+
// Set database status to SYNCING
|
| 82 |
await db.update(users)
|
| 83 |
.set({
|
| 84 |
syncStatus: 'SYNCING',
|
| 85 |
syncError: null
|
| 86 |
})
|
| 87 |
.where(eq(users.id, user.id));
|
|
|
|
| 88 |
|
| 89 |
console.log(`[SyncRun] Starting sync for user ${user.id}`);
|
| 90 |
|
|
|
|
| 109 |
// Always reconcile the critical openTriage#1 issue to ensure immediate state sync
|
| 110 |
const reconcileResult = await reconcileOpenTriageIssue1(user.githubAccessToken);
|
| 111 |
|
|
|
|
|
|
|
| 112 |
// Mark sync as COMPLETED
|
| 113 |
await db.update(users).set({
|
| 114 |
syncStatus: 'COMPLETED',
|
| 115 |
lastSyncAt: new Date().toISOString(),
|
| 116 |
syncError: null
|
| 117 |
}).where(eq(users.id, user.id));
|
|
|
|
| 118 |
|
| 119 |
console.log(`[SyncRun] Sync completed for user ${user.id}`);
|
| 120 |
|
|
|
|
| 133 |
} catch (syncError: any) {
|
| 134 |
console.error(`[SyncRun] Error syncing for user ${user.id}:`, syncError);
|
| 135 |
|
| 136 |
+
// Mark sync as FAILED with error message
|
|
|
|
|
|
|
| 137 |
await db.update(users).set({
|
| 138 |
syncStatus: 'FAILED',
|
| 139 |
syncError: syncError.message || 'Unknown error'
|
| 140 |
}).where(eq(users.id, user.id));
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
return NextResponse.json({
|
| 143 |
error: "Sync failed",
|
| 144 |
message: syncError.message || "Internal server error"
|
| 145 |
}, { status: 500 });
|
| 146 |
+
} finally {
|
| 147 |
+
// CRITICAL: Always remove from in-memory set, even on error
|
| 148 |
+
activeSyncs.delete(user.id);
|
| 149 |
+
console.log(`[SyncRun] Removed user ${user.id} from activeSyncs. Size: ${activeSyncs.size}`);
|
| 150 |
}
|
| 151 |
} catch (error) {
|
| 152 |
console.error("POST /api/sync/run error:", error);
|
src/app/api/sync/status/route.ts
CHANGED
|
@@ -1,25 +1,51 @@
|
|
| 1 |
/**
|
| 2 |
-
*
|
| 3 |
*
|
| 4 |
-
*
|
| 5 |
-
*
|
| 6 |
-
*
|
| 7 |
-
* To re-enable:
|
| 8 |
-
* 1. Run migration: add_sync_status.sql on Turso
|
| 9 |
-
* 2. Uncomment the code below
|
| 10 |
-
* 3. Deploy
|
| 11 |
*/
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
}
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
|
|
|
| 1 |
/**
|
| 2 |
+
* Sync Status API Route
|
| 3 |
*
|
| 4 |
+
* GET /api/sync/status - Check current sync status for the authenticated user
|
| 5 |
+
* Used by frontend to poll sync progress without triggering new syncs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
*/
|
| 7 |
|
| 8 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 9 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 10 |
+
import { db } from "@/db";
|
| 11 |
+
import { users } from "@/db/schema";
|
| 12 |
+
import { eq } from "drizzle-orm";
|
| 13 |
+
|
| 14 |
+
export async function GET(request: NextRequest) {
|
| 15 |
+
try {
|
| 16 |
+
const user = await getCurrentUser(request);
|
| 17 |
+
if (!user) {
|
| 18 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// Get user's sync status
|
| 22 |
+
const userRecord = await db.select({
|
| 23 |
+
syncStatus: users.syncStatus,
|
| 24 |
+
lastSyncAt: users.lastSyncAt,
|
| 25 |
+
syncError: users.syncError,
|
| 26 |
+
})
|
| 27 |
+
.from(users)
|
| 28 |
+
.where(eq(users.id, user.id))
|
| 29 |
+
.limit(1);
|
| 30 |
+
|
| 31 |
+
if (!userRecord || userRecord.length === 0) {
|
| 32 |
+
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
| 33 |
}
|
| 34 |
+
|
| 35 |
+
const { syncStatus, lastSyncAt, syncError } = userRecord[0];
|
| 36 |
+
|
| 37 |
+
return NextResponse.json({
|
| 38 |
+
status: syncStatus || 'IDLE',
|
| 39 |
+
lastSyncAt: lastSyncAt || null,
|
| 40 |
+
error: syncError || null,
|
| 41 |
+
isIdle: syncStatus === 'IDLE' || syncStatus === 'COMPLETED',
|
| 42 |
+
isSyncing: syncStatus === 'SYNCING',
|
| 43 |
+
isPending: syncStatus === 'PENDING',
|
| 44 |
+
hasFailed: syncStatus === 'FAILED',
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
} catch (error) {
|
| 48 |
+
console.error("GET /api/sync/status error:", error);
|
| 49 |
+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
| 50 |
+
}
|
| 51 |
}
|
src/db/schema.ts
CHANGED
|
@@ -39,11 +39,10 @@ export const users = sqliteTable("users", {
|
|
| 39 |
role: text("role"), // email: text("email"),
|
| 40 |
githubAccessToken: text("github_access_token"),
|
| 41 |
|
| 42 |
-
//
|
| 43 |
-
//
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
// syncError: text("sync_error"), // Error message if failed
|
| 47 |
|
| 48 |
createdAt: text("created_at").notNull(),
|
| 49 |
updatedAt: text("updated_at").notNull(),
|
|
|
|
| 39 |
role: text("role"), // email: text("email"),
|
| 40 |
githubAccessToken: text("github_access_token"),
|
| 41 |
|
| 42 |
+
// Sync status tracking (Phase 2) - Re-enabled after Turso migration
|
| 43 |
+
syncStatus: text("sync_status").default("IDLE"), // IDLE | PENDING | SYNCING | COMPLETED | FAILED
|
| 44 |
+
lastSyncAt: text("last_sync_at"), // ISO timestamp
|
| 45 |
+
syncError: text("sync_error"), // Error message if failed
|
|
|
|
| 46 |
|
| 47 |
createdAt: text("created_at").notNull(),
|
| 48 |
updatedAt: text("updated_at").notNull(),
|
src/lib/auth.ts
CHANGED
|
@@ -66,7 +66,6 @@ export async function getCurrentUser(request: NextRequest) {
|
|
| 66 |
console.log("[getCurrentUser] Token verified, user_id:", payload.user_id);
|
| 67 |
|
| 68 |
// Fetch full user from database
|
| 69 |
-
// TODO: After Turso migration, re-add syncStatus, lastSyncAt, syncError to this select
|
| 70 |
const userRecords = await db
|
| 71 |
.select({
|
| 72 |
id: users.id,
|
|
@@ -75,6 +74,9 @@ export async function getCurrentUser(request: NextRequest) {
|
|
| 75 |
avatarUrl: users.avatarUrl,
|
| 76 |
role: users.role,
|
| 77 |
githubAccessToken: users.githubAccessToken,
|
|
|
|
|
|
|
|
|
|
| 78 |
createdAt: users.createdAt,
|
| 79 |
updatedAt: users.updatedAt,
|
| 80 |
})
|
|
|
|
| 66 |
console.log("[getCurrentUser] Token verified, user_id:", payload.user_id);
|
| 67 |
|
| 68 |
// Fetch full user from database
|
|
|
|
| 69 |
const userRecords = await db
|
| 70 |
.select({
|
| 71 |
id: users.id,
|
|
|
|
| 74 |
avatarUrl: users.avatarUrl,
|
| 75 |
role: users.role,
|
| 76 |
githubAccessToken: users.githubAccessToken,
|
| 77 |
+
syncStatus: users.syncStatus,
|
| 78 |
+
lastSyncAt: users.lastSyncAt,
|
| 79 |
+
syncError: users.syncError,
|
| 80 |
createdAt: users.createdAt,
|
| 81 |
updatedAt: users.updatedAt,
|
| 82 |
})
|