* feat(demo): add DEMO_MODE runtime helpers and /api/demo/info endpoint * feat(demo): enforce simulated and blocked API actions under DEMO_MODE * feat(demo): add deterministic seed dataset and 6-hour auto-reset * feat(demo-ui): add demo banner and custom sonner toasts for simulated/blocked actions * test+docs(demo): add demo mode coverage, behavior matrix, and operator docs * formatting * tests * feat(demo): seed resets from typed baseline defaults * formatting * feat(demo): enrich baseline seed data and demo project catalog * feat(demo): expand seeded applications and chart time ranges * refactor(demo): split demo seed data from generation logic * feat(demo): cap generated application history to 30 days * feat(demo): rebalance generated job status distribution * feat(demo-ui): make demo banner fixed and topmost * minor fixes * formatting * duration revert * durations * feat(demo): share demo info hook, brighten demo toasts, and enforce webhook auth * comment explaning * formatting * comments * deadline builder comment
211 lines
5.9 KiB
TypeScript
211 lines
5.9 KiB
TypeScript
import { okWithMeta } from "@infra/http";
|
|
import { getSetting } from "@server/repositories/settings";
|
|
import { LlmService } from "@server/services/llm-service";
|
|
import { RxResumeClient } from "@server/services/rxresume-client";
|
|
import {
|
|
getResume,
|
|
RxResumeCredentialsError,
|
|
} from "@server/services/rxresume-v4";
|
|
import { resumeDataSchema } from "@shared/rxresume-schema";
|
|
import { type Request, type Response, Router } from "express";
|
|
import { isDemoMode } from "../../config/demo";
|
|
|
|
export const onboardingRouter = Router();
|
|
|
|
type ValidationResponse = {
|
|
valid: boolean;
|
|
message: string | null;
|
|
};
|
|
|
|
async function validateLlm(options: {
|
|
apiKey?: string | null;
|
|
provider?: string | null;
|
|
baseUrl?: string | null;
|
|
}): Promise<ValidationResponse> {
|
|
const llm = new LlmService({
|
|
apiKey: options.apiKey,
|
|
provider: options.provider ?? undefined,
|
|
baseUrl: options.baseUrl ?? undefined,
|
|
});
|
|
return llm.validateCredentials();
|
|
}
|
|
|
|
/**
|
|
* Validate that a base resume is configured and accessible via RxResume v4 API.
|
|
*/
|
|
async function validateResumeConfig(): Promise<ValidationResponse> {
|
|
try {
|
|
// Check if rxresumeBaseResumeId is configured
|
|
const rxresumeBaseResumeId = await getSetting("rxresumeBaseResumeId");
|
|
|
|
if (!rxresumeBaseResumeId) {
|
|
return {
|
|
valid: false,
|
|
message:
|
|
"No base resume selected. Please select a resume from your RxResume account in Settings.",
|
|
};
|
|
}
|
|
|
|
// Verify the resume is accessible and valid
|
|
try {
|
|
const resume = await getResume(rxresumeBaseResumeId);
|
|
|
|
if (!resume.data || typeof resume.data !== "object") {
|
|
return {
|
|
valid: false,
|
|
message: "Selected resume is empty or invalid.",
|
|
};
|
|
}
|
|
|
|
// Validate against schema
|
|
const result = resumeDataSchema.safeParse(resume.data);
|
|
if (!result.success) {
|
|
const issue = result.error.issues[0];
|
|
const path = issue?.path?.join(".") || "";
|
|
const baseMessage =
|
|
issue?.message ?? "Resume does not match the expected schema.";
|
|
const details = path ? `Field "${path}": ${baseMessage}` : baseMessage;
|
|
return { valid: false, message: details };
|
|
}
|
|
|
|
return { valid: true, message: null };
|
|
} catch (error) {
|
|
if (error instanceof RxResumeCredentialsError) {
|
|
return {
|
|
valid: false,
|
|
message: "RxResume credentials not configured.",
|
|
};
|
|
}
|
|
const message =
|
|
error instanceof Error
|
|
? error.message
|
|
: "Failed to fetch resume from RxResume.";
|
|
return { valid: false, message };
|
|
}
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error ? error.message : "Resume validation failed.";
|
|
return { valid: false, message };
|
|
}
|
|
}
|
|
|
|
async function validateRxresume(
|
|
email?: string | null,
|
|
password?: string | null,
|
|
): Promise<ValidationResponse> {
|
|
const rxEmail = email?.trim() || process.env.RXRESUME_EMAIL || "";
|
|
const rxPassword = password?.trim() || process.env.RXRESUME_PASSWORD || "";
|
|
|
|
if (!rxEmail || !rxPassword) {
|
|
return { valid: false, message: "RxResume credentials are missing." };
|
|
}
|
|
|
|
const result = await RxResumeClient.verifyCredentials(rxEmail, rxPassword);
|
|
|
|
if (result.ok) {
|
|
return { valid: true, message: null };
|
|
}
|
|
|
|
const normalizedMessage = result.message?.toLowerCase() ?? "";
|
|
if (
|
|
result.status === 401 ||
|
|
normalizedMessage.includes("invalidcredentials")
|
|
) {
|
|
return {
|
|
valid: false,
|
|
message:
|
|
"Invalid RxResume credentials. Check your email and password and try again.",
|
|
};
|
|
}
|
|
|
|
const message =
|
|
result.message || `RxResume validation failed (HTTP ${result.status})`;
|
|
return { valid: false, message };
|
|
}
|
|
|
|
onboardingRouter.post(
|
|
"/validate/openrouter",
|
|
async (req: Request, res: Response) => {
|
|
if (isDemoMode()) {
|
|
return okWithMeta(
|
|
res,
|
|
{
|
|
valid: true,
|
|
message:
|
|
"Demo mode: OpenRouter validation is simulated and always succeeds.",
|
|
},
|
|
{ simulated: true },
|
|
);
|
|
}
|
|
|
|
const apiKey =
|
|
typeof req.body?.apiKey === "string" ? req.body.apiKey : undefined;
|
|
const result = await validateLlm({ apiKey, provider: "openrouter" });
|
|
res.json({ success: true, data: result });
|
|
},
|
|
);
|
|
|
|
onboardingRouter.post("/validate/llm", async (req: Request, res: Response) => {
|
|
if (isDemoMode()) {
|
|
return okWithMeta(
|
|
res,
|
|
{
|
|
valid: true,
|
|
message: "Demo mode: LLM validation is simulated.",
|
|
},
|
|
{ simulated: true },
|
|
);
|
|
}
|
|
|
|
const apiKey =
|
|
typeof req.body?.apiKey === "string" ? req.body.apiKey : undefined;
|
|
const provider =
|
|
typeof req.body?.provider === "string" ? req.body.provider : undefined;
|
|
const baseUrl =
|
|
typeof req.body?.baseUrl === "string" ? req.body.baseUrl : undefined;
|
|
const result = await validateLlm({ apiKey, provider, baseUrl });
|
|
res.json({ success: true, data: result });
|
|
});
|
|
|
|
onboardingRouter.post(
|
|
"/validate/rxresume",
|
|
async (req: Request, res: Response) => {
|
|
if (isDemoMode()) {
|
|
return okWithMeta(
|
|
res,
|
|
{
|
|
valid: true,
|
|
message: "Demo mode: RxResume validation is simulated.",
|
|
},
|
|
{ simulated: true },
|
|
);
|
|
}
|
|
|
|
const email =
|
|
typeof req.body?.email === "string" ? req.body.email : undefined;
|
|
const password =
|
|
typeof req.body?.password === "string" ? req.body.password : undefined;
|
|
const result = await validateRxresume(email, password);
|
|
res.json({ success: true, data: result });
|
|
},
|
|
);
|
|
|
|
onboardingRouter.get(
|
|
"/validate/resume",
|
|
async (_req: Request, res: Response) => {
|
|
if (isDemoMode()) {
|
|
return okWithMeta(
|
|
res,
|
|
{
|
|
valid: true,
|
|
message: "Demo mode: resume validation is simulated.",
|
|
},
|
|
{ simulated: true },
|
|
);
|
|
}
|
|
|
|
const result = await validateResumeConfig();
|
|
res.json({ success: true, data: result });
|
|
},
|
|
);
|