Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions src/app/api/debug/health/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { NextResponse } from "next/server";
import { supabaseAdmin } from "@/lib/supabase";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";

export const dynamic = "force-dynamic";

/**
* Debug endpoint to check database connectivity and user creation
* Remove this endpoint in production
*/
export async function GET() {
const health = {
status: "checking",
timestamp: new Date().toISOString(),
checks: {} as Record<string, any>,
};

try {
// Check 1: Supabase client initialization
health.checks.supabaseClient = {
status: "ok",
message: "Client initialized",
};

// Check 2: Database connectivity
try {
const { data, error } = await supabaseAdmin.from("users").select("count").single();
if (error) {
health.checks.database = {
status: "error",
message: error.message,
code: error.code,
};
} else {
health.checks.database = {
status: "ok",
message: "Database connected",
};
}
} catch (e) {
health.checks.database = {
status: "error",
message: String(e),
};
}

// Check 3: Authentication
const session = await getServerSession(authOptions);
if (session?.githubId) {
health.checks.session = {
status: "ok",
githubId: session.githubId,
githubLogin: session.githubLogin,
};

// Check 4: Lookup existing user
try {
const { data: user, error } = await supabaseAdmin
.from("users")
.select("id, github_id, github_login")
.eq("github_id", session.githubId)
.single();

if (error?.code === "PGRST116") {
health.checks.userLookup = {
status: "not_found",
message: "User does not exist (PGRST116)",
};
} else if (error) {
health.checks.userLookup = {
status: "error",
message: error.message,
code: error.code,
};
} else if (user) {
health.checks.userLookup = {
status: "found",
userId: user.id,
githubId: user.github_id,
githubLogin: user.github_login,
};
}
} catch (e) {
health.checks.userLookup = {
status: "error",
message: String(e),
};
}
} else {
health.checks.session = {
status: "not_authenticated",
message: "No active session",
};
}

health.status = "ok";
return NextResponse.json(health);
} catch (error) {
health.status = "error";
health.checks.fatal = {
error: String(error),
message: error instanceof Error ? error.message : "Unknown error",
};
return NextResponse.json(health, { status: 500 });
}
}

44 changes: 28 additions & 16 deletions src/app/api/goals/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,36 @@ export async function DELETE(
_req: Request,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });
const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) {
console.error("Failed to resolve user for goals DELETE:", {
githubId: session.githubId,
});
return Response.json({ error: "User not found" }, { status: 404 });
}

// Only delete if the goal belongs to the authenticated user
const { error } = await supabaseAdmin
.from("goals")
.delete()
.eq("id", params.id)
.eq("user_id", user.id);
// Only delete if the goal belongs to the authenticated user
const { error } = await supabaseAdmin
.from("goals")
.delete()
.eq("id", params.id)
.eq("user_id", user.id);

if (error) {
return Response.json({ error: "Failed to delete goal" }, { status: 500 });
}
if (error) {
console.error("Error deleting goal:", error);
return Response.json({ error: "Failed to delete goal" }, { status: 500 });
}

return Response.json({ success: true }, { status: 200 });
return Response.json({ success: true }, { status: 200 });
} catch (error) {
console.error("Unexpected error in goals DELETE:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}

131 changes: 75 additions & 56 deletions src/app/api/goals/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,62 +48,81 @@ function getPeriodStart(recurrence: Recurrence): string {
}

export async function GET() {
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) return Response.json({ error: "User not found" }, { status: 404 });

const { data: goals } = await supabaseAdmin
.from("goals")
.select("*")
.eq("user_id", user.id)
.order("created_at", { ascending: false });

// Reset progress if we're in a new period
const processedGoals = await Promise.all(
(goals ?? []).map(async (goal: Goal) => {
if (goal.recurrence === "none") return goal;

const periodStart = new Date(getPeriodStart(goal.recurrence as Recurrence));
const storedPeriodStart = goal.period_start
? new Date(goal.period_start)
: new Date(0);

if (storedPeriodStart < periodStart) {
// Use a conditional update that only succeeds when the DB row still
// has the old period_start. If two concurrent GET requests both see
// a stale period_start and race to reset the goal, only one update
// will match the lt() filter — the second finds no row and returns
// null, after which we re-fetch the already-reset row to avoid
// silently zeroing out any progress written between the two reads.
const { data: updated } = await supabaseAdmin
.from("goals")
.update({ current: 0, period_start: periodStart.toISOString() })
.eq("id", goal.id)
.lt("period_start", periodStart.toISOString())
.select()
.single();

if (updated) return updated;

// Another concurrent request already reset this goal — re-fetch
// the current state so we return accurate data without clobbering it.
const { data: current } = await supabaseAdmin
.from("goals")
.select("*")
.eq("id", goal.id)
.single();
return current ?? goal;
}

return goal;
})
);
try {
const session = await getServerSession(authOptions);
if (!session?.githubId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const user = await resolveAppUser(session.githubId, session.githubLogin);
if (!user) {
console.error("Failed to resolve user for goals GET:", { githubId: session.githubId });
return Response.json({ error: "User not found" }, { status: 404 });
}

const { data: goals, error: fetchError } = await supabaseAdmin
.from("goals")
.select("*")
.eq("user_id", user.id)
.order("created_at", { ascending: false });

if (fetchError) {
console.error("Error fetching goals:", fetchError);
return Response.json(
{ error: "Failed to fetch goals" },
{ status: 500 }
);
}

// Reset progress if we're in a new period
const processedGoals = await Promise.all(
(goals ?? []).map(async (goal: Goal) => {
if (goal.recurrence === "none") return goal;

const periodStart = new Date(getPeriodStart(goal.recurrence as Recurrence));
const storedPeriodStart = goal.period_start
? new Date(goal.period_start)
: new Date(0);

if (storedPeriodStart < periodStart) {
// Use a conditional update that only succeeds when the DB row still
// has the old period_start. If two concurrent GET requests both see
// a stale period_start and race to reset the goal, only one update
// will match the lt() filter — the second finds no row and returns
// null, after which we re-fetch the already-reset row to avoid
// silently zeroing out any progress written between the two reads.
const { data: updated } = await supabaseAdmin
.from("goals")
.update({ current: 0, period_start: periodStart.toISOString() })
.eq("id", goal.id)
.lt("period_start", periodStart.toISOString())
.select()
.single();

if (updated) return updated;

// Another concurrent request already reset this goal — re-fetch
// the current state so we return accurate data without clobbering it.
const { data: current } = await supabaseAdmin
.from("goals")
.select("*")
.eq("id", goal.id)
.single();
return current ?? goal;
}

return goal;
})
);

return Response.json({ goals: processedGoals });
return Response.json({ goals: processedGoals });
} catch (error) {
console.error("Unexpected error in goals GET:", error);
return Response.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

export async function POST(req: Request) {
Expand Down Expand Up @@ -168,4 +187,4 @@ export async function POST(req: Request) {
if (error) return Response.json({ error: error.message }, { status: 500 });

return Response.json({ goal }, { status: 201 });
}
}
Loading
Loading