app/routes/api.jobs.tsx (186 lines of code) (raw):
import {
json,
type LoaderFunctionArgs,
type ActionFunctionArgs,
} from "@remix-run/node";
import { getJobStore } from "~/lib/server/jobStore";
import { getJobProcessor } from "~/lib/server/jobProcessor";
import {
extractCredentialsFromCookie,
isPublicPath,
hasValidCredentials,
getEffectiveUsername,
} from "~/lib/server/auth";
import serverConfig from "~/lib/server/config";
import { v4 as uuidv4 } from "uuid";
import type { Job } from "~/types/job";
// GET /api/jobs - List jobs
export async function loader({ request }: LoaderFunctionArgs) {
console.log("GET /api/jobs");
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") || "1");
const limit = Math.min(parseInt(url.searchParams.get("limit") || "20"), 100);
const status = url.searchParams.get("status");
const search = url.searchParams.get("search");
// Check authentication
if (!isPublicPath(url.pathname)) {
const cookieHeader = request.headers.get("Cookie");
const credentials = extractCredentialsFromCookie(cookieHeader);
if (!hasValidCredentials(credentials)) {
return json(
{
error: {
code: "UNAUTHORIZED",
message:
"Authentication required - please provide API credentials through the UI",
},
},
{ status: 401 }
);
}
try {
const jobStore = getJobStore();
// Get the authenticated user's username
// Use effective username which can be from HF or GitHub
const username = getEffectiveUsername(credentials);
// Add the author filter to only show the user's own jobs
const result = await jobStore.listJobs({
page,
limit,
status: status === "all" ? undefined : status || undefined,
search: search || undefined,
author: username, // Filter by the authenticated user
});
return json(result);
} catch (error) {
console.error("Error listing jobs:", error);
return json(
{
error: {
code: "INTERNAL_ERROR",
message: "Failed to list jobs",
},
},
{ status: 500 }
);
}
} else {
// For public paths, only return minimal information
return json({
jobs: [],
pagination: {
page: 1,
limit,
total: 0,
totalPages: 0,
hasNext: false,
hasPrev: false,
},
});
}
}
// POST /api/jobs - Create job
export async function action({ request }: ActionFunctionArgs) {
console.log("POST /api/jobs");
if (request.method !== "POST") {
return json({ error: "Method not allowed" }, { status: 405 });
}
// Check authentication
const cookieHeader = request.headers.get("Cookie");
const credentials = extractCredentialsFromCookie(cookieHeader);
if (!hasValidCredentials(credentials)) {
return json(
{
error: {
code: "UNAUTHORIZED",
message:
"Authentication required - please provide API credentials through the UI",
},
},
{ status: 401 }
);
}
try {
const { title, description, branch, author, repository } =
await request.json();
// Validation
if (!title || title.length === 0 || title.length > 200) {
return json(
{
error: {
code: "VALIDATION_ERROR",
message: "Title is required and must be between 1-200 characters",
details: [
{
field: "title",
message:
"Title is required and must be between 1-200 characters",
},
],
},
},
{ status: 400 }
);
}
if (description && description.length > 1000) {
return json(
{
error: {
code: "VALIDATION_ERROR",
message: "Description must be less than 1000 characters",
details: [
{
field: "description",
message: "Description must be less than 1000 characters",
},
],
},
},
{ status: 400 }
);
}
// Validate repository URL if provided
if (repository?.url) {
const githubUrlPattern =
/^https:\/\/github\.com\/[\w\-\.]+\/[\w\-\.]+(?:\.git)?(?:\/)?$/;
if (!githubUrlPattern.test(repository.url)) {
return json(
{
error: {
code: "VALIDATION_ERROR",
message: "Repository URL must be a valid GitHub repository URL",
details: [
{
field: "repository.url",
message:
"Repository URL must be a valid GitHub repository URL (e.g., https://github.com/username/repo)",
},
],
},
},
{ status: 400 }
);
}
}
// Use the authenticated user as the author if not provided
// Support both HuggingFace and GitHub usernames
const authenticatedAuthor = getEffectiveUsername(credentials);
const job: Job = {
id: uuidv4(),
title,
description: description || "",
status: "pending",
createdAt: new Date(),
updatedAt: new Date(),
branch: branch || undefined,
author: author || authenticatedAuthor, // Use authenticated user if author not provided
repository: repository || undefined,
};
const jobStore = getJobStore();
await jobStore.createJob(job);
// Start processing job asynchronously with credentials
const jobProcessor = getJobProcessor();
jobProcessor.processJob(job.id, jobStore, credentials).catch((error) => {
console.error(`Failed to process job ${job.id}:`, error);
});
return json(job, { status: 201 });
} catch (error) {
console.error("Error creating job:", error);
return json(
{
error: {
code: "INTERNAL_ERROR",
message: "Failed to create job",
},
},
{ status: 500 }
);
}
}