app/routes/api.auth.github.validate-pat.tsx (69 lines of code) (raw):
import { ActionFunction, json } from "@remix-run/node";
export const action: ActionFunction = async ({ request }) => {
if (request.method !== "POST") {
return json({ error: "Method not allowed" }, { status: 405 });
}
try {
const { token } = await request.json();
// tokens start with ghp_ or github_pat
if (
!token ||
(!token.startsWith("ghp_") && !token.startsWith("github_pat"))
) {
console.log("Invalid token format");
return json(
{
error:
'Invalid token format. GitHub Personal Access Tokens should start with "ghp_"',
},
{ status: 400 }
);
}
// Validate token by making a test API call
const response = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (!response.ok) {
if (response.status === 401) {
return json({ error: "Invalid or expired token" }, { status: 400 });
}
return json(
{ error: "Failed to validate token with GitHub" },
{ status: 400 }
);
}
const userData = await response.json();
// Check if token has repo scope by testing access to a private repo endpoint
const scopeResponse = await fetch(
"https://api.github.com/user/repos?type=private&per_page=1",
{
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
}
);
if (!scopeResponse.ok) {
return json(
{
error:
'Token does not have required "repo" scope. Please create a new token with repo access.',
},
{ status: 400 }
);
}
return json({
valid: true,
user: {
username: userData.login,
name: userData.name || userData.login,
avatar_url: userData.avatar_url,
},
});
} catch (error) {
console.error("PAT validation error:", error);
return json({ error: "Failed to validate token" }, { status: 500 });
}
};