Skip to content
Merged
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
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key

# -------------------------------------------------------
# NextAuth
# The URL your app is running on
# MUST match the deployed origin exactly (no trailing slash).
# Local: http://localhost:3000
# Production: https://devtrack-delta.vercel.app (set this in Vercel env vars)
# Wrong value causes OAuth callback URL mismatch → error=github on sign-in.
NEXTAUTH_URL=http://localhost:3000

# Generate with: openssl rand -base64 32
Expand Down
67 changes: 46 additions & 21 deletions src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { supabaseAdmin } from "./supabase";

const SESSION_MAX_AGE = 30 * 24 * 60 * 60;
const SESSION_UPDATE_AGE = 24 * 60 * 60;
const useSecureCookies = process.env.NODE_ENV === "production";
const useSecureCookies = process.env.NEXTAUTH_URL?.startsWith("https://") ?? process.env.NODE_ENV === "production";

const GITHUB_API = "https://api.github.com";
// Re-validate the stored GitHub token at most once every 24 hours per session.
Expand All @@ -24,8 +24,19 @@ export const authOptions: NextAuthOptions = {
}),
],
pages: {
signIn: "/auth/signin",
},
signIn: "/auth/signin",
},
cookies: {
sessionToken: {
name: `${useSecureCookies ? "__Secure-" : ""}next-auth.session-token`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
},
session: {
strategy: "jwt",
maxAge: SESSION_MAX_AGE,
Expand All @@ -38,26 +49,40 @@ export const authOptions: NextAuthOptions = {
async signIn({ account, profile }) {
if (account?.provider === "github" && profile) {
const p = profile as { id: number; login: string };
const { data: user } = await supabaseAdmin.from("users").upsert(
{
github_id: String(p.id),
github_login: p.login,
updated_at: new Date().toISOString(),
},
{ onConflict: "github_id" }
).select("id").single();
try {
const { data: user, error: upsertError } = await supabaseAdmin
.from("users")
.upsert(
{
github_id: String(p.id),
github_login: p.login,
updated_at: new Date().toISOString(),
},
{ onConflict: "github_id" }
)
.select("id")
.single();

if (upsertError) {
console.error("[auth] Supabase upsert error:", upsertError);
}

if (user?.id && account.access_token) {
try {
await syncGitHubAchievementsForUser({
userId: user.id,
githubLogin: p.login,
token: account.access_token,
force: true,
});
} catch (error) {
console.error("GitHub achievements sync failed:", error);
if (user?.id && account.access_token) {
try {
await syncGitHubAchievementsForUser({
userId: user.id,
githubLogin: p.login,
token: account.access_token,
force: true,
});
} catch (error) {
console.error("[auth] GitHub achievements sync failed:", error);
}
}
} catch (error) {
// Database failures must not block sign-in — the user is authenticated
// by GitHub; local sync is best-effort.
console.error("[auth] signIn callback error (non-fatal):", error);
}
}
return true;
Expand Down
Loading