app/routes/api.auth.tsx (212 lines of code) (raw):
import { ActionFunction, json } from "@remix-run/node";
import serverConfig from "~/lib/server/config";
import {
extractCredentialsFromCookie,
hasValidCredentials,
hasGitHubCredentials,
} from "~/lib/server/auth";
export interface ApiCredentials {
openaiApiKey: string;
huggingfaceToken: string;
hfUserInfo: HFUserInfo;
}
export interface HFUserInfo {
username: string;
fullName: string;
avatarUrl: string;
}
export interface AuthRequest {
action: "authenticate" | "logout" | "verify";
credentials?: {
openaiApiKey?: string;
huggingfaceToken?: string;
};
}
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days in seconds
export const action: ActionFunction = async ({ request }) => {
const body: AuthRequest = await request.json();
switch (body.action) {
case "authenticate":
return await handleAuthenticate(body.credentials, request);
case "logout":
return handleLogout();
case "verify":
return await handleVerify(request);
default:
return json({ error: "Invalid action" }, { status: 400 });
}
};
async function handleAuthenticate(
credentials: AuthRequest["credentials"],
request: Request
) {
if (!credentials) {
return json({ error: "No credentials provided" }, { status: 400 });
}
try {
// Validate credentials format
if (!validateCredentials(credentials)) {
return json({ error: "Invalid credentials format" }, { status: 400 });
}
// Test credentials with API calls
const testResult = await testCredentials(credentials);
if (!testResult.isValid) {
return json({ error: "Invalid API credentials" }, { status: 401 });
}
// Create cookie data
const expiresAt = new Date();
expiresAt.setTime(expiresAt.getTime() + COOKIE_MAX_AGE * 1000);
const cookieData = {
hasOpenAI: false, // OpenAI is now handled as a regular secret, not in auth
hasHuggingFace: true,
expiresAt: expiresAt.toISOString(),
hfUserInfo: testResult.hfUserInfo
? btoa(JSON.stringify(testResult.hfUserInfo))
: null,
// Store encrypted HuggingFace token only
enc: btoa(
JSON.stringify({
hf: credentials.huggingfaceToken || "",
})
),
};
// Create cookie value
const cookieValue = btoa(JSON.stringify(cookieData));
// Set cookie with proper headers
const headers = new Headers();
// Determine if we should use Secure flag
const url = new URL(request.url);
const isSecure = url.protocol === "https:";
// const cookieOptions = [
// `${config.COOKIE_NAME}=${cookieValue}`,
// `Max-Age=${COOKIE_MAX_AGE}`,
// "Path=/",
// // "SameSite=None",
// // "HttpOnly=false", // Need to access from client-side
// ];
// // const cookieOptions = [
// // `${config.COOKIE_NAME}=${cookieValue}`,
// // `Max-Age=${COOKIE_MAX_AGE}`,
// // "Path=/",
// // "SameSite=Lax",
// // "HttpOnly=false", // Need to access from client-side
// // ];
// if (isSecure) {
// cookieOptions.push("Secure");
// }
const cookieOptions = [
`${serverConfig.COOKIE_NAME}=${cookieValue}`,
`Max-Age=${COOKIE_MAX_AGE}`,
"Path=/",
// "SameSite=Lax", // Lax is more compatible than None
"SameSite=None", // Lax is more compatible than None
// Don't set HttpOnly to allow client-side access
"Secure", // Only set Secure in HTTPS environments
"HttpOnly=true", // Allow client-side access
];
// Only add Secure in production HTTPS environments
if (isSecure) {
cookieOptions.push("Secure");
}
// // For HTTPS environments
// if (isSecure) {
// cookieOptions.push("Secure");
// cookieOptions.push("SameSite=None"); // When Secure is used, SameSite=None allows cross-origin requests
// } else {
// cookieOptions.push("SameSite=Lax"); // For non-HTTPS environments
// }
headers.set("Set-Cookie", cookieOptions.join("; "));
return json(
{
success: true,
authStatus: {
isAuthenticated: true,
hasOpenAI: false, // OpenAI is now a regular secret
hasHuggingFace: true,
expiresAt,
hfUserInfo: testResult.hfUserInfo,
},
},
{ headers }
);
} catch (error) {
console.error("Authentication failed:", error);
return json({ error: "Authentication failed" }, { status: 500 });
}
}
function handleLogout() {
const headers = new Headers();
// Clear cookie by setting it to expire
const cookieOptions = [
`${serverConfig.COOKIE_NAME}=`,
"expires=Thu, 01 Jan 1970 00:00:00 UTC",
"Path=/",
"SameSite=None", // Allow cross-origin requests
"Secure", // Only set Secure in HTTPS environments
"HttpOnly=true", // Allow client-side access
];
headers.set("Set-Cookie", cookieOptions.join("; "));
return json(
{ success: true, message: "Logged out successfully" },
{ headers }
);
}
async function handleVerify(request: Request) {
const cookieHeader = request.headers.get("Cookie");
if (!cookieHeader) {
return json({
isAuthenticated: false,
hasOpenAI: false,
hasHuggingFace: false,
hasGitHub: false,
});
}
// Use the improved credential extraction logic
const credentials = extractCredentialsFromCookie(cookieHeader);
// Check if the user has valid credentials (HF or GitHub in Docker mode)
const isAuthenticated = hasValidCredentials(credentials);
const hasGitHub = hasGitHubCredentials(credentials);
const hasHuggingFace = !!credentials.huggingfaceToken;
if (!isAuthenticated) {
return json({
isAuthenticated: false,
hasOpenAI: false,
hasHuggingFace: false,
hasGitHub: false,
});
}
return json({
isAuthenticated: true,
hasOpenAI: false, // OpenAI is handled as a regular secret now
hasHuggingFace,
hasGitHub,
hfUserInfo: credentials.hfUserInfo,
githubUserInfo: credentials.githubUserInfo,
});
}
function validateCredentials(credentials: any): boolean {
// OpenAI API key validation (starts with sk-) - optional
if (credentials.openaiApiKey && !credentials.openaiApiKey.startsWith("sk-")) {
return false;
}
// HuggingFace token validation (starts with hf_) - required
if (
!credentials.huggingfaceToken ||
!credentials.huggingfaceToken.startsWith("hf_")
) {
return false;
}
return true;
}
async function testCredentials(credentials: any): Promise<{
isValid: boolean;
hfUserInfo?: HFUserInfo;
}> {
let hfUserInfo: HFUserInfo | undefined;
// Test HuggingFace API - required
try {
hfUserInfo = await testHuggingFaceToken(credentials.huggingfaceToken);
if (!hfUserInfo) {
return { isValid: false };
}
} catch (error) {
return { isValid: false };
}
// OpenAI key validation is optional - we assume it's valid if provided
// to avoid rate limiting issues
return { isValid: true, hfUserInfo };
}
async function testHuggingFaceToken(token: string): Promise<HFUserInfo | null> {
try {
const response = await fetch("https://huggingface.co/api/whoami-v2", {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
console.error("HuggingFace API response not OK:", response.status);
return null;
}
const data = await response.json();
return {
username: data.name || "",
fullName: data.fullname || "",
avatarUrl: data.avatarUrl || "",
};
} catch (error) {
console.error("HuggingFace API test failed:", error);
return null;
}
}
function parseCookies(cookieHeader: string): Record<string, string> {
const cookies: Record<string, string> = {};
cookieHeader.split(";").forEach((cookie) => {
const [name, ...rest] = cookie.trim().split("=");
if (name && rest.length > 0) {
// Handle both encoded and non-encoded cookie names
const decodedName = decodeURIComponent(name);
const cookieValue = rest.join("=");
cookies[name] = cookieValue; // Store with original name
cookies[decodedName] = cookieValue; // Store with decoded name
}
});
return cookies;
}