initial commit of v5

This commit is contained in:
Amruth Pillai
2026-01-19 23:31:54 +01:00
parent 55bdfd0067
commit cad390fa13
1132 changed files with 200807 additions and 165288 deletions
+176
View File
@@ -0,0 +1,176 @@
import { createAnthropic } from "@ai-sdk/anthropic";
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { createOpenAI } from "@ai-sdk/openai";
import { createGateway, generateText, Output, streamText } from "ai";
import { createOllama } from "ai-sdk-ollama";
import { match } from "ts-pattern";
import z, { flattenError, ZodError } from "zod";
import docxParserSystemPrompt from "@/integrations/ai/prompts/docx-parser-system.md?raw";
import docxParserUserPrompt from "@/integrations/ai/prompts/docx-parser-user.md?raw";
import pdfParserSystemPrompt from "@/integrations/ai/prompts/pdf-parser-system.md?raw";
import pdfParserUserPrompt from "@/integrations/ai/prompts/pdf-parser-user.md?raw";
import { defaultResumeData, resumeDataSchema } from "@/schema/resume/data";
import { protectedProcedure } from "../context";
const aiProviderSchema = z.enum(["vercel-ai-gateway", "openai", "anthropic", "gemini", "ollama"]);
type AIProvider = z.infer<typeof aiProviderSchema>;
type GetModelInput = {
provider: AIProvider;
model: string;
apiKey: string;
baseURL?: string;
};
function getModel(input: GetModelInput) {
const { provider, model, apiKey, baseURL } = input;
return match(provider)
.with("vercel-ai-gateway", () => createGateway({ apiKey }).languageModel(model))
.with("openai", () => createOpenAI({ apiKey }).languageModel(model))
.with("anthropic", () => createAnthropic({ apiKey }).languageModel(model))
.with("gemini", () => createGoogleGenerativeAI({ apiKey }).languageModel(model))
.with("ollama", () => createOllama({ apiKey, baseURL }).languageModel(model))
.exhaustive();
}
const aiCredentialsSchema = z.object({
provider: aiProviderSchema,
model: z.string(),
apiKey: z.string(),
baseURL: z.string().optional(),
});
const fileInputSchema = z.object({
name: z.string(),
data: z.string(), // base64 encoded
});
export const aiRouter = {
testConnection: protectedProcedure
.input(
z.object({
provider: aiProviderSchema,
model: z.string(),
apiKey: z.string(),
baseURL: z.string().optional(),
}),
)
.handler(async function* ({ input }) {
const stream = streamText({
temperature: 0,
model: getModel(input),
messages: [{ role: "user", content: 'Respond with "1"' }],
});
yield* stream.textStream;
}),
parsePdf: protectedProcedure
.input(
z.object({
...aiCredentialsSchema.shape,
file: fileInputSchema,
}),
)
.handler(async ({ input }) => {
try {
const model = getModel(input);
const result = await generateText({
model,
maxRetries: 0,
output: Output.object({
schema: resumeDataSchema.omit({ picture: true, metadata: true, customSections: true }),
}),
messages: [
{
role: "system",
content: pdfParserSystemPrompt,
},
{
role: "user",
content: [
{ type: "text", text: pdfParserUserPrompt },
{
type: "file",
filename: input.file.name,
mediaType: "application/pdf",
data: input.file.data,
},
],
},
],
});
return resumeDataSchema.parse({
...result.output,
customSections: [],
picture: defaultResumeData.picture,
metadata: defaultResumeData.metadata,
});
} catch (error) {
if (error instanceof ZodError) {
const errors = flattenError(error);
throw new Error(JSON.stringify(errors));
}
throw error;
}
}),
parseDocx: protectedProcedure
.input(
z.object({
...aiCredentialsSchema.shape,
file: fileInputSchema,
mediaType: z.enum([
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
]),
}),
)
.handler(async ({ input }) => {
try {
const model = getModel(input);
const result = await generateText({
model,
maxRetries: 0,
output: Output.object({
schema: resumeDataSchema.omit({ picture: true, metadata: true, customSections: true }),
}),
messages: [
{ role: "system", content: docxParserSystemPrompt },
{
role: "user",
content: [
{ type: "text", text: docxParserUserPrompt },
{
type: "file",
filename: input.file.name,
mediaType: input.mediaType,
data: input.file.data,
},
],
},
],
});
return resumeDataSchema.parse({
...result.output,
customSections: [],
picture: defaultResumeData.picture,
metadata: defaultResumeData.metadata,
});
} catch (error) {
if (error instanceof ZodError) {
const errors = flattenError(error);
throw new Error(JSON.stringify(errors));
}
throw error;
}
}),
};
+56
View File
@@ -0,0 +1,56 @@
import z from "zod";
import { protectedProcedure, publicProcedure } from "../context";
import { authService, type ProviderList } from "../services/auth";
export const authRouter = {
providers: {
list: publicProcedure
.route({
method: "GET",
path: "/auth/providers/list",
tags: ["Authentication"],
summary: "List all auth providers",
description:
"A list of all authentication providers, and their display names, supported by the instance of Reactive Resume.",
})
.handler((): ProviderList => {
return authService.providers.list();
}),
},
verifyResumePassword: publicProcedure
.route({
method: "POST",
path: "/auth/verify-resume-password",
tags: ["Authentication", "Resume"],
summary: "Verify resume password",
description: "Verify a resume password, to grant access to the locked resume.",
})
.input(
z.object({
slug: z.string().min(1),
username: z.string().min(1),
password: z.string().min(1),
}),
)
.output(z.boolean())
.handler(async ({ input }): Promise<boolean> => {
return await authService.verifyResumePassword({
slug: input.slug,
username: input.username,
password: input.password,
});
}),
deleteAccount: protectedProcedure
.route({
method: "DELETE",
path: "/auth/delete-account",
tags: ["Authentication"],
summary: "Delete user account",
description: "Delete the authenticated user's account and all associated data.",
})
.handler(async ({ context }): Promise<void> => {
return await authService.deleteAccount({ userId: context.user.id });
}),
};
+15
View File
@@ -0,0 +1,15 @@
import { aiRouter } from "./ai";
import { authRouter } from "./auth";
import { printerRouter } from "./printer";
import { resumeRouter } from "./resume";
import { statisticsRouter } from "./statistics";
import { storageRouter } from "./storage";
export default {
ai: aiRouter,
auth: authRouter,
resume: resumeRouter,
storage: storageRouter,
printer: printerRouter,
statistics: statisticsRouter,
};
+51
View File
@@ -0,0 +1,51 @@
import z from "zod";
import { protectedProcedure, publicProcedure } from "../context";
import { printerService } from "../services/printer";
import { resumeService } from "../services/resume";
export const printerRouter = {
printResumeAsPDF: publicProcedure
.route({
method: "GET",
path: "/printer/resume/{id}/pdf",
tags: ["Resume", "Printer"],
summary: "Export resume as PDF",
description: "Export a resume as a PDF. Returns a URL to download the PDF.",
})
.input(z.object({ id: z.string() }))
.output(z.object({ url: z.string() }))
.handler(async ({ input, context }) => {
// Get resume to find the owner's userId for storage key
const resume = await resumeService.getByIdForPrinter({ id: input.id });
const url = await printerService.printResumeAsPDF({
id: input.id,
userId: resume.userId,
});
if (!context.user) {
await resumeService.statistics.increment({ id: input.id, downloads: true });
}
return { url };
}),
getResumeScreenshot: protectedProcedure
.route({
method: "GET",
path: "/printer/resume/{id}/screenshot",
tags: ["Resume", "Printer"],
summary: "Get resume screenshot",
description: "Get a screenshot of a resume. Returns a URL to the screenshot image.",
})
.input(z.object({ id: z.string() }))
.output(z.object({ url: z.string() }))
.handler(async ({ input, context }) => {
const url = await printerService.getResumeScreenshot({
id: input.id,
userId: context.user.id,
});
return { url };
}),
};
+331
View File
@@ -0,0 +1,331 @@
import z from "zod";
import { resumeDataSchema, sampleResumeData } from "@/schema/resume/data";
import { generateRandomName, slugify } from "@/utils/string";
import { protectedProcedure, publicProcedure, serverOnlyProcedure } from "../context";
import { resumeService } from "../services/resume";
const tagsRouter = {
list: protectedProcedure
.route({
method: "GET",
path: "/resume/tags/list",
tags: ["Resume"],
summary: "List all resume tags",
description: "List all tags for the authenticated user's resumes. Used to populate the filter in the dashboard.",
})
.output(z.array(z.string()))
.handler(async ({ context }) => {
return await resumeService.tags.list({ userId: context.user.id });
}),
};
const statisticsRouter = {
getById: protectedProcedure
.route({
method: "GET",
path: "/resume/statistics/{id}",
tags: ["Resume"],
summary: "Get resume statistics",
description: "Get the statistics for a resume, such as number of views and downloads.",
})
.input(z.object({ id: z.string() }))
.output(
z.object({
isPublic: z.boolean(),
views: z.number(),
downloads: z.number(),
lastViewedAt: z.date().nullable(),
lastDownloadedAt: z.date().nullable(),
}),
)
.handler(async ({ context, input }) => {
return await resumeService.statistics.getById({ id: input.id, userId: context.user.id });
}),
increment: publicProcedure
.route({ tags: ["Internal"], summary: "Increment resume statistics" })
.input(z.object({ id: z.string(), views: z.boolean().default(false), downloads: z.boolean().default(false) }))
.handler(async ({ input }) => {
return await resumeService.statistics.increment(input);
}),
};
export const resumeRouter = {
tags: tagsRouter,
statistics: statisticsRouter,
list: protectedProcedure
.route({
method: "GET",
path: "/resume/list",
tags: ["Resume"],
summary: "List all resumes",
description: "List of all the resumes for the authenticated user.",
})
.input(
z
.object({
tags: z.array(z.string()).optional().default([]),
sort: z.enum(["lastUpdatedAt", "createdAt", "name"]).optional().default("lastUpdatedAt"),
})
.optional()
.default({ tags: [], sort: "lastUpdatedAt" }),
)
.output(
z.array(
z.object({
id: z.string(),
name: z.string(),
slug: z.string(),
tags: z.array(z.string()),
isPublic: z.boolean(),
isLocked: z.boolean(),
createdAt: z.date(),
updatedAt: z.date(),
}),
),
)
.handler(async ({ input, context }) => {
return await resumeService.list({
userId: context.user.id,
tags: input.tags,
sort: input.sort,
});
}),
getById: protectedProcedure
.route({
method: "GET",
path: "/resume/{id}",
tags: ["Resume"],
summary: "Get resume by ID",
description: "Get a resume, along with its data, by its ID.",
})
.input(z.object({ id: z.string() }))
.output(
z.object({
id: z.string(),
name: z.string(),
slug: z.string(),
tags: z.array(z.string()),
data: resumeDataSchema,
isPublic: z.boolean(),
isLocked: z.boolean(),
hasPassword: z.boolean(),
}),
)
.handler(async ({ context, input }) => {
return await resumeService.getById({ id: input.id, userId: context.user.id });
}),
getByIdForPrinter: serverOnlyProcedure
.route({ tags: ["Internal"], summary: "Get resume by ID for printer" })
.input(z.object({ id: z.string() }))
.handler(async ({ input }) => {
return await resumeService.getByIdForPrinter({ id: input.id });
}),
getBySlug: publicProcedure
.route({
method: "GET",
path: "/resume/{username}/{slug}",
tags: ["Resume"],
summary: "Get resume by username and slug",
description: "Get a resume, along with its data, by its username and slug.",
})
.input(z.object({ username: z.string(), slug: z.string() }))
.output(
z.object({
id: z.string(),
name: z.string(),
slug: z.string(),
tags: z.array(z.string()),
data: resumeDataSchema,
isPublic: z.boolean(),
isLocked: z.boolean(),
}),
)
.handler(async ({ input, context }) => {
return await resumeService.getBySlug({ ...input, currentUserId: context.user?.id });
}),
create: protectedProcedure
.route({
method: "POST",
path: "/resume/create",
tags: ["Resume"],
summary: "Create a new resume",
description: "Create a new resume, with the ability to initialize it with sample data.",
})
.input(
z.object({
name: z.string().min(1).max(64),
slug: z.string().min(1).max(64),
tags: z.array(z.string()),
withSampleData: z.boolean().default(false),
}),
)
.output(z.string().describe("The ID of the created resume."))
.handler(async ({ context, input }) => {
return await resumeService.create({
name: input.name,
slug: input.slug,
tags: input.tags,
locale: context.locale,
userId: context.user.id,
data: input.withSampleData ? sampleResumeData : undefined,
});
}),
import: protectedProcedure
.route({
method: "POST",
path: "/resume/import",
tags: ["Resume"],
summary: "Import a resume",
description: "Import a resume from a file.",
})
.input(z.object({ data: resumeDataSchema }))
.output(z.string().describe("The ID of the imported resume."))
.handler(async ({ context, input }) => {
const name = generateRandomName();
const slug = slugify(name);
return await resumeService.create({
name,
slug,
tags: [],
data: input.data,
locale: context.locale,
userId: context.user.id,
});
}),
update: protectedProcedure
.route({
method: "PUT",
path: "/resume/{id}",
tags: ["Resume"],
summary: "Update a resume",
description: "Update a resume, along with its data, by its ID.",
})
.input(
z.object({
id: z.string(),
name: z.string().optional(),
slug: z.string().optional(),
tags: z.array(z.string()).optional(),
data: resumeDataSchema.optional(),
isPublic: z.boolean().optional(),
}),
)
.output(z.void())
.handler(async ({ context, input }) => {
return await resumeService.update({
id: input.id,
userId: context.user.id,
name: input.name,
slug: input.slug,
tags: input.tags,
data: input.data,
isPublic: input.isPublic,
});
}),
setLocked: protectedProcedure
.route({
method: "POST",
path: "/resume/{id}/set-locked",
tags: ["Resume"],
summary: "Set resume locked status",
description: "Toggle the locked status of a resume, by its ID.",
})
.input(z.object({ id: z.string(), isLocked: z.boolean() }))
.output(z.void())
.handler(async ({ context, input }) => {
return await resumeService.setLocked({
id: input.id,
userId: context.user.id,
isLocked: input.isLocked,
});
}),
setPassword: protectedProcedure
.route({
method: "POST",
path: "/resume/{id}/set-password",
tags: ["Resume"],
summary: "Set password on a resume",
description: "Set a password on a resume to protect it from unauthorized access when shared publicly.",
})
.input(z.object({ id: z.string(), password: z.string().min(6).max(64) }))
.output(z.void())
.handler(async ({ context, input }) => {
return await resumeService.setPassword({
id: input.id,
userId: context.user.id,
password: input.password,
});
}),
removePassword: protectedProcedure
.route({
method: "POST",
path: "/resume/{id}/remove-password",
tags: ["Resume"],
summary: "Remove password from a resume",
description: "Remove password protection from a resume.",
})
.input(z.object({ id: z.string() }))
.output(z.void())
.handler(async ({ context, input }) => {
return await resumeService.removePassword({
id: input.id,
userId: context.user.id,
});
}),
duplicate: protectedProcedure
.route({
method: "POST",
path: "/resume/{id}/duplicate",
tags: ["Resume"],
summary: "Duplicate a resume",
description: "Duplicate a resume, by its ID.",
})
.input(
z.object({
id: z.string(),
name: z.string().optional(),
slug: z.string().optional(),
tags: z.array(z.string()).optional(),
}),
)
.output(z.string().describe("The ID of the duplicated resume."))
.handler(async ({ context, input }) => {
const original = await resumeService.getById({ id: input.id, userId: context.user.id });
return await resumeService.create({
userId: context.user.id,
name: input.name ?? original.name,
slug: input.slug ?? original.slug,
tags: input.tags ?? original.tags,
locale: context.locale,
data: original.data,
});
}),
delete: protectedProcedure
.route({
method: "DELETE",
path: "/resume/{id}",
tags: ["Resume"],
summary: "Delete a resume",
description: "Delete a resume, by its ID.",
})
.input(z.object({ id: z.string() }))
.output(z.void())
.handler(async ({ context, input }) => {
return await resumeService.delete({ id: input.id, userId: context.user.id });
}),
};
@@ -0,0 +1,54 @@
import z from "zod";
import { publicProcedure } from "../context";
import { statisticsService } from "../services/statistics";
const userRouter = {
getCount: publicProcedure
.route({
method: "GET",
path: "/statistics/user/count",
tags: ["Statistics"],
summary: "Get total number of users",
description: "Get the total number of users for the Reactive Resume.",
})
.output(z.number())
.handler(async (): Promise<number> => {
return await statisticsService.user.getCount();
}),
};
const resumeRouter = {
getCount: publicProcedure
.route({
method: "GET",
path: "/statistics/resume/count",
tags: ["Statistics"],
summary: "Get total number of resumes",
description: "Get the total number of resumes for the Reactive Resume.",
})
.output(z.number())
.handler(async (): Promise<number> => {
return await statisticsService.resume.getCount();
}),
};
const githubRouter = {
getStarCount: publicProcedure
.route({
method: "GET",
path: "/statistics/github/stars",
tags: ["Statistics"],
summary: "Get GitHub Repository stargazers count",
description: "Get the stargazers count for the Reactive Resume GitHub repository, at the time of writing.",
})
.output(z.number())
.handler(async (): Promise<number> => {
return await statisticsService.github.getStarCount();
}),
};
export const statisticsRouter = {
user: userRouter,
resume: resumeRouter,
github: githubRouter,
};
+69
View File
@@ -0,0 +1,69 @@
import { ORPCError } from "@orpc/server";
import z from "zod";
import { protectedProcedure } from "../context";
import { getStorageService, isImageFile, processImageForUpload, uploadFile } from "../services/storage";
const storageService = getStorageService();
const fileSchema = z.file().max(10 * 1024 * 1024, "File size must be less than 10MB");
const filenameSchema = z.object({ filename: z.string().min(1) });
export const storageRouter = {
uploadFile: protectedProcedure
.route({ tags: ["Internal"], summary: "Upload a file" })
.input(fileSchema)
.output(
z.object({
url: z.string(),
path: z.string(),
contentType: z.string(),
}),
)
.handler(async ({ context, input: file }) => {
const originalMimeType = file.type;
const isImage = isImageFile(originalMimeType);
let data: Uint8Array;
let contentType: string;
if (isImage) {
const processed = await processImageForUpload(file);
data = processed.data;
contentType = processed.contentType;
} else {
const fileBuffer = await file.arrayBuffer();
data = new Uint8Array(fileBuffer);
contentType = originalMimeType;
}
const result = await uploadFile({
userId: context.user.id,
data,
contentType,
type: "picture",
});
return {
url: result.url,
path: result.key,
contentType,
};
}),
deleteFile: protectedProcedure
.route({ tags: ["Internal"], summary: "Delete a file" })
.input(filenameSchema)
.output(z.void())
.handler(async ({ context, input }): Promise<void> => {
// The filename is now the full path from the URL (e.g., "uploads/userId/pictures/timestamp.webp")
// We need to extract just the path portion that matches the storage key
const key = input.filename.startsWith("uploads/")
? input.filename
: `uploads/${context.user.id}/pictures/${input.filename}`;
const deleted = await storageService.delete(key);
if (!deleted) throw new ORPCError("NOT_FOUND");
}),
};