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
@@ -0,0 +1,75 @@
You are a specialized resume parsing assistant that converts Microsoft Word (DOC/DOCX) resumes into a structured JSON format compatible with Reactive Resume. Your primary directive is accuracy and faithfulness to the source document.
## CRITICAL RULES
### Anti-Hallucination Guidelines
1. **Extract ONLY information explicitly present in the resume** - Never invent, assume, or infer data that isn't clearly stated
2. **When uncertain, omit rather than guess** - Leave fields empty ("") or use empty arrays ([]) rather than fabricating content
3. **Preserve original wording** - Use the exact text from the resume; do not paraphrase, embellish, or "improve" the content
4. **Do not fill gaps** - If a date range is missing an end date, leave it empty; if a job title seems incomplete, use what's provided
5. **No external knowledge** - Do not add information about companies, schools, or technologies that isn't in the resume itself
6. **Ignore formatting artifacts** - Word documents may contain hidden formatting, track changes, comments, or metadata. Extract only visible, intended content
### Data Extraction Rules
- **Dates**: Use only dates explicitly stated. Do not calculate or estimate dates. Use the format provided in the resume.
- **URLs**: Only include URLs that are explicitly written in the resume. Do not construct URLs from usernames or company names.
- **Contact Information**: Extract only what is explicitly provided. Do not format or standardize phone numbers beyond what's shown.
- **Skills**: List only skills explicitly mentioned. Do not infer skills from job descriptions or technologies mentioned in passing.
- **Descriptions**: Convert to HTML format but preserve the original content exactly. Use <p> for paragraphs and <ul><li> for bullet points.
- **Tables and Lists**: Extract content from Word tables and lists accurately. Preserve the structure but convert to appropriate HTML format.
- **Headers and Footers**: Only extract content from headers/footers if it contains resume-relevant information (like contact details). Ignore page numbers and document metadata.
### Required Field Handling
- Generate UUIDs for all \`id\` fields (use format: lowercase alphanumeric, 8-12 characters)
- Set \`hidden: false\` for all items unless the resume explicitly indicates something should be hidden
- Use \`columns: 1\` as default for sections unless multi-column layout is clearly appropriate
- For \`website\` objects, use \`{"url": "", "label": ""}\` when no URL is provided
### Section Mapping Guide
Map resume content to these sections based on explicit section headers or clear context:
- **basics**: Name, title/headline, email, phone, location (city/state/country)
- **summary**: Professional summary, objective, about me, profile
- **experience**: Work experience, employment history, professional experience
- **education**: Education, academic background, qualifications
- **skills**: Skills, technical skills, competencies, expertise
- **projects**: Projects, portfolio, personal projects
- **certifications**: Certifications, licenses, credentials
- **awards**: Awards, honors, achievements, recognition
- **languages**: Languages, language proficiency
- **volunteer**: Volunteer experience, community involvement
- **publications**: Publications, articles, papers
- **references**: References (often "Available upon request")
- **profiles**: Social media links, online profiles (LinkedIn, GitHub, etc.)
- **interests**: Interests, hobbies (only if explicitly listed)
### Word Document Specific Considerations
- **Styles and Formatting**: Ignore Word-specific formatting (styles, themes, fonts). Focus on content structure and hierarchy.
- **Track Changes**: Ignore any tracked changes or comments. Extract only the final, accepted version of the text.
- **Hyperlinks**: Extract hyperlink URLs only if they are explicitly visible in the document. Do not extract hidden hyperlinks.
- **Tables**: Extract table content accurately, converting to appropriate structured format. Preserve relationships between table cells.
- **Multi-column Layouts**: Recognize multi-column sections and extract content in the correct order (left to right, top to bottom).
- **Text Boxes and Shapes**: Extract content from text boxes and shapes if they contain resume-relevant information.
### Output Requirements
1. Output ONLY valid JSON - no markdown code blocks, no explanations, no comments
2. The JSON must strictly conform to the provided schema
3. All required fields must be present, even if empty
4. Use empty strings ("") for missing text fields
5. Use empty arrays ([]) for missing array fields
### What NOT To Do
- ❌ Do not add job responsibilities that aren't listed
- ❌ Do not expand acronyms unless the expansion is provided
- ❌ Do not add technologies to skills that are only mentioned in job descriptions
- ❌ Do not create profile URLs from usernames (e.g., don't create "github.com/username" unless the full URL is provided)
- ❌ Do not assume current employment - only mark as "Present" if the resume explicitly says so
- ❌ Do not add metrics or achievements not explicitly stated
- ❌ Do not standardize or reformat dates beyond basic consistency
- ❌ Do not translate content to another language - preserve the original language
- ❌ Do not extract hidden text, comments, or tracked changes
- ❌ Do not infer information from Word document properties or metadata
- ❌ Do not extract content from headers/footers unless it's clearly resume content (ignore page numbers, document paths, etc.)
## OUTPUT
Respond with ONLY the JSON object. No preamble, no explanation, no markdown formatting.
@@ -0,0 +1 @@
Here is the Microsoft Word resume document to parse. Parse it carefully following all rules above, extracting only visible content and ignoring any formatting artifacts, track changes, or hidden metadata.
@@ -0,0 +1,61 @@
You are a specialized resume parsing assistant that converts PDF resumes into a structured JSON format compatible with Reactive Resume. Your primary directive is accuracy and faithfulness to the source document.
## CRITICAL RULES
### Anti-Hallucination Guidelines
1. **Extract ONLY information explicitly present in the resume** - Never invent, assume, or infer data that isn't clearly stated
2. **When uncertain, omit rather than guess** - Leave fields empty ("") or use empty arrays ([]) rather than fabricating content
3. **Preserve original wording** - Use the exact text from the resume; do not paraphrase, embellish, or "improve" the content
4. **Do not fill gaps** - If a date range is missing an end date, leave it empty; if a job title seems incomplete, use what's provided
5. **No external knowledge** - Do not add information about companies, schools, or technologies that isn't in the resume itself
### Data Extraction Rules
- **Dates**: Use only dates explicitly stated. Do not calculate or estimate dates. Use the format provided in the resume.
- **URLs**: Only include URLs that are explicitly written in the resume. Do not construct URLs from usernames or company names.
- **Contact Information**: Extract only what is explicitly provided. Do not format or standardize phone numbers beyond what's shown.
- **Skills**: List only skills explicitly mentioned. Do not infer skills from job descriptions or technologies mentioned in passing.
- **Descriptions**: Convert to HTML format but preserve the original content exactly. Use <p> for paragraphs and <ul><li> for bullet points.
### Required Field Handling
- Generate UUIDs for all \`id\` fields (use format: lowercase alphanumeric, 8-12 characters)
- Set \`hidden: false\` for all items unless the resume explicitly indicates something should be hidden
- Use \`columns: 1\` as default for sections unless multi-column layout is clearly appropriate
- For \`website\` objects, use \`{"url": "", "label": ""}\` when no URL is provided
### Section Mapping Guide
Map resume content to these sections based on explicit section headers or clear context:
- **basics**: Name, title/headline, email, phone, location (city/state/country)
- **summary**: Professional summary, objective, about me, profile
- **experience**: Work experience, employment history, professional experience
- **education**: Education, academic background, qualifications
- **skills**: Skills, technical skills, competencies, expertise
- **projects**: Projects, portfolio, personal projects
- **certifications**: Certifications, licenses, credentials
- **awards**: Awards, honors, achievements, recognition
- **languages**: Languages, language proficiency
- **volunteer**: Volunteer experience, community involvement
- **publications**: Publications, articles, papers
- **references**: References (often "Available upon request")
- **profiles**: Social media links, online profiles (LinkedIn, GitHub, etc.)
- **interests**: Interests, hobbies (only if explicitly listed)
### Output Requirements
1. Output ONLY valid JSON - no markdown code blocks, no explanations, no comments
2. The JSON must strictly conform to the provided schema
3. All required fields must be present, even if empty
4. Use empty strings ("") for missing text fields
5. Use empty arrays ([]) for missing array fields
### What NOT To Do
- ❌ Do not add job responsibilities that aren't listed
- ❌ Do not expand acronyms unless the expansion is provided
- ❌ Do not add technologies to skills that are only mentioned in job descriptions
- ❌ Do not create profile URLs from usernames (e.g., don't create "github.com/username" unless the full URL is provided)
- ❌ Do not assume current employment - only mark as "Present" if the resume explicitly says so
- ❌ Do not add metrics or achievements not explicitly stated
- ❌ Do not standardize or reformat dates beyond basic consistency
- ❌ Do not translate content to another language - preserve the original language
## OUTPUT
Respond with ONLY the JSON object. No preamble, no explanation, no markdown formatting.
@@ -0,0 +1 @@
Here is the PDF resume document to parse. Parse it carefully following all rules above, extracting only information that is explicitly visible in the PDF. Ignore any artifacts caused by PDF formatting, scanned image noise, OCR errors, watermarks, or hidden content. Do not infer or assume any information beyond what is clearly present in the visible text of the PDF.
+89
View File
@@ -0,0 +1,89 @@
import type { WritableDraft } from "immer";
import { createJSONStorage, persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
import { create } from "zustand/react";
export type AIProvider = "vercel-ai-gateway" | "openai" | "gemini" | "anthropic" | "ollama";
export type TestStatus = "unverified" | "success" | "failure";
type AIStoreState = {
enabled: boolean;
provider: AIProvider;
model: string;
apiKey: string;
baseURL: string;
testStatus: TestStatus;
};
type AIStoreActions = {
canEnable: () => boolean;
setEnabled: (value: boolean) => void;
set: (fn: (draft: WritableDraft<AIStoreState>) => void) => void;
reset: () => void;
};
type AIStore = AIStoreState & AIStoreActions;
const initialState: AIStoreState = {
enabled: false,
provider: "openai",
model: "",
apiKey: "",
baseURL: "",
testStatus: "unverified",
};
export const useAIStore = create<AIStore>()(
persist(
immer((set, get) => ({
...initialState,
set: (fn) => {
set((draft) => {
const prev = {
provider: draft.provider,
model: draft.model,
apiKey: draft.apiKey,
baseURL: draft.baseURL,
};
fn(draft);
if (
draft.provider !== prev.provider ||
draft.model !== prev.model ||
draft.apiKey !== prev.apiKey ||
draft.baseURL !== prev.baseURL
) {
draft.testStatus = "unverified";
draft.enabled = false;
}
});
},
reset: () => set(() => initialState),
canEnable: () => {
const { testStatus } = get();
return testStatus === "success";
},
setEnabled: (value: boolean) => {
const canEnable = get().canEnable();
if (value && !canEnable) return;
set((draft) => {
draft.enabled = value;
});
},
})),
{
name: "ai-store",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
enabled: state.enabled,
provider: state.provider,
model: state.model,
apiKey: state.apiKey,
baseURL: state.baseURL,
testStatus: state.testStatus,
}),
},
),
);
+32
View File
@@ -0,0 +1,32 @@
import { passkeyClient } from "@better-auth/passkey/client";
import {
apiKeyClient,
genericOAuthClient,
inferAdditionalFields,
twoFactorClient,
usernameClient,
} from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
import type { auth } from "./config";
const getAuthClient = () => {
return createAuthClient({
plugins: [
apiKeyClient(),
usernameClient(),
twoFactorClient({
onTwoFactorRedirect() {
// Redirect to 2FA verification page
if (typeof window !== "undefined") {
window.location.href = "/auth/verify-2fa";
}
},
}),
passkeyClient(),
genericOAuthClient(),
inferAdditionalFields<typeof auth>(),
],
});
};
export const authClient = getAuthClient();
+198
View File
@@ -0,0 +1,198 @@
import { BetterAuthError } from "@better-auth/core/error";
import { passkey } from "@better-auth/passkey";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { betterAuth } from "better-auth/minimal";
import { apiKey, type GenericOAuthConfig, genericOAuth, twoFactor } from "better-auth/plugins";
import { username } from "better-auth/plugins/username";
import { tanstackStartCookies } from "better-auth/tanstack-start";
import { db } from "@/integrations/drizzle/client";
import { env } from "@/utils/env";
import { hashPassword, verifyPassword } from "@/utils/password";
import { generateId, toUsername } from "@/utils/string";
import { schema } from "../drizzle";
import { sendEmail } from "../email/service";
function isCustomOAuthProviderEnabled() {
const hasDiscovery = Boolean(env.OAUTH_DISCOVERY_URL);
const hasManual =
Boolean(env.OAUTH_AUTHORIZATION_URL) && Boolean(env.OAUTH_TOKEN_URL) && Boolean(env.OAUTH_USER_INFO_URL);
return Boolean(env.OAUTH_CLIENT_ID) && Boolean(env.OAUTH_CLIENT_SECRET) && (hasDiscovery || hasManual);
}
const getAuthConfig = () => {
const authConfigs: GenericOAuthConfig[] = [];
if (isCustomOAuthProviderEnabled()) {
authConfigs.push({
providerId: "custom",
clientId: env.OAUTH_CLIENT_ID as string,
clientSecret: env.OAUTH_CLIENT_SECRET as string,
discoveryUrl: env.OAUTH_DISCOVERY_URL,
authorizationUrl: env.OAUTH_AUTHORIZATION_URL,
tokenUrl: env.OAUTH_TOKEN_URL,
userInfoUrl: env.OAUTH_USER_INFO_URL,
scopes: env.OAUTH_SCOPES,
redirectURI: `${env.APP_URL}/api/auth/oauth2/callback/custom`,
mapProfileToUser: async (profile) => {
if (!profile.email) {
throw new BetterAuthError(
"OAuth Provider did not return an email address. This is required for user creation.",
{ cause: "EMAIL_REQUIRED" },
);
}
const email = profile.email;
const name = profile.name ?? profile.preferred_username ?? email.split("@")[0];
const username = profile.preferred_username ?? email.split("@")[0];
const image = profile.image ?? profile.picture ?? profile.avatar_url;
return {
name,
email,
image,
username,
displayUsername: username,
emailVerified: true,
};
},
} satisfies GenericOAuthConfig);
}
return betterAuth({
appName: "Reactive Resume",
baseURL: env.APP_URL,
secret: env.AUTH_SECRET,
database: drizzleAdapter(db, { schema, provider: "pg" }),
telemetry: { enabled: false },
trustedOrigins: [env.APP_URL],
advanced: {
database: { generateId },
useSecureCookies: env.APP_URL.startsWith("https://"),
},
emailAndPassword: {
enabled: true,
autoSignIn: true,
minPasswordLength: 6,
maxPasswordLength: 64,
requireEmailVerification: false,
disableSignUp: env.FLAG_DISABLE_SIGNUP,
sendResetPassword: async ({ user, url }) => {
await sendEmail({
to: user.email,
subject: "Reset your password",
text: `To reset your password, please visit the following URL: ${url}. If you did not request a password reset, please ignore this email.`,
});
},
password: {
hash: (password) => hashPassword(password),
verify: ({ password, hash }) => verifyPassword(password, hash),
},
},
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
sendVerificationEmail: async ({ user, url }) => {
await sendEmail({
to: user.email,
subject: "Verify your email",
text: `You recently signed up for an account on Reactive Resume.\nTo verify your email, please visit the following URL: ${url}`,
});
},
},
user: {
changeEmail: {
enabled: true,
sendChangeEmailVerification: async ({ user, newEmail, url }) => {
await sendEmail({
to: newEmail,
subject: "Verify your new email",
text: `You recently requested to change your email on Reactive Resume from ${user.email} to ${newEmail}.\nTo verify this change, please visit the following URL: ${url}\nIf you did not request this change, please ignore this email.`,
});
},
},
additionalFields: {
username: {
type: "string",
required: true,
},
},
},
account: {
accountLinking: {
enabled: true,
trustedProviders: ["google", "github"],
},
},
socialProviders: {
google: {
enabled: !!env.GOOGLE_CLIENT_ID && !!env.GOOGLE_CLIENT_SECRET,
// biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null
clientId: env.GOOGLE_CLIENT_ID!,
// biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null
clientSecret: env.GOOGLE_CLIENT_SECRET!,
mapProfileToUser: async (profile) => {
return {
name: profile.name,
email: profile.email,
image: profile.picture,
username: profile.email.split("@")[0],
displayUsername: profile.email.split("@")[0],
emailVerified: true,
};
},
},
github: {
enabled: !!env.GITHUB_CLIENT_ID && !!env.GITHUB_CLIENT_SECRET,
// biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null
clientId: env.GITHUB_CLIENT_ID!,
// biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null
clientSecret: env.GITHUB_CLIENT_SECRET!,
mapProfileToUser: async (profile) => {
return {
name: profile.name,
email: profile.email,
image: profile.avatar_url,
username: profile.login,
displayUsername: profile.login,
emailVerified: true,
};
},
},
},
plugins: [
apiKey(),
username({
minUsernameLength: 3,
maxUsernameLength: 64,
usernameNormalization: (value) => toUsername(value),
displayUsernameNormalization: (value) => toUsername(value),
validationOrder: { username: "post-normalization", displayUsername: "post-normalization" },
}),
twoFactor({
issuer: "Reactive Resume",
}),
passkey({
rpName: "Reactive Resume",
rpID: new URL(env.APP_URL).hostname,
origin: env.APP_URL,
}),
genericOAuth({
config: authConfigs,
}),
tanstackStartCookies(),
],
});
};
export const auth = getAuthConfig();
+16
View File
@@ -0,0 +1,16 @@
import { createIsomorphicFn } from "@tanstack/react-start";
import { getRequestHeaders } from "@tanstack/react-start/server";
import { authClient } from "./client";
import { auth } from "./config";
import type { AuthSession } from "./types";
export const getSession = createIsomorphicFn()
.client(async (): Promise<AuthSession | null> => {
const { data, error } = await authClient.getSession();
if (error) return null;
return data;
})
.server(async (): Promise<AuthSession | null> => {
const result = await auth.api.getSession({ headers: getRequestHeaders() });
return result;
});
+8
View File
@@ -0,0 +1,8 @@
import z from "zod";
import type { auth } from "./config";
export type AuthSession = typeof auth.$Infer.Session;
const authProviderSchema = z.enum(["credential", "google", "github", "custom"]);
export type AuthProvider = z.infer<typeof authProviderSchema>;
+34
View File
@@ -0,0 +1,34 @@
import { createServerOnlyFn } from "@tanstack/react-start";
import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { schema } from "@/integrations/drizzle";
import { env } from "@/utils/env";
// During hot reload (i.e., in development), global assignment ensures the pool/client persist across reloads.
// This prevents exhausting connection limits due to re-creation on every reload.
declare global {
var __pool: Pool | undefined;
var __drizzle: NodePgDatabase<typeof schema> | undefined;
}
function getPool() {
if (!globalThis.__pool) {
globalThis.__pool = new Pool({ connectionString: env.DATABASE_URL });
}
return globalThis.__pool;
}
function makeDrizzleClient() {
const pool = getPool();
return drizzle({ client: pool, schema });
}
const getDatabaseServerFn = createServerOnlyFn(() => {
if (!globalThis.__drizzle) {
globalThis.__drizzle = makeDrizzleClient();
}
return globalThis.__drizzle;
});
export const db = getDatabaseServerFn();
+1
View File
@@ -0,0 +1 @@
export * as schema from "./schema";
+264
View File
@@ -0,0 +1,264 @@
import * as pg from "drizzle-orm/pg-core";
import { defaultResumeData, type ResumeData } from "@/schema/resume/data";
import { generateId } from "@/utils/string";
export const user = pg.pgTable(
"user",
{
id: pg
.uuid("id")
.notNull()
.primaryKey()
.$defaultFn(() => generateId()),
image: pg.text("image"),
name: pg.text("name").notNull(),
email: pg.text("email").notNull().unique(),
emailVerified: pg.boolean("email_verified").notNull().default(false),
username: pg.text("username").notNull().unique(),
displayUsername: pg.text("display_username").notNull().unique(),
twoFactorEnabled: pg.boolean("two_factor_enabled").notNull().default(false),
createdAt: pg.timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: pg
.timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()),
},
(t) => [pg.index().on(t.email), pg.index().on(t.username)],
);
export const session = pg.pgTable(
"session",
{
id: pg
.uuid("id")
.notNull()
.primaryKey()
.$defaultFn(() => generateId()),
token: pg.text("token").notNull().unique(),
ipAddress: pg.text("ip_address"),
userAgent: pg.text("user_agent"),
userId: pg
.uuid("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
expiresAt: pg.timestamp("expires_at", { withTimezone: true }).notNull(),
createdAt: pg.timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: pg
.timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()),
},
(t) => [pg.index().on(t.token, t.userId), pg.index().on(t.expiresAt)],
);
export const account = pg.pgTable(
"account",
{
id: pg
.uuid("id")
.notNull()
.primaryKey()
.$defaultFn(() => generateId()),
accountId: pg.text("account_id").notNull(),
providerId: pg.text("provider_id").notNull().default("credential"),
userId: pg
.uuid("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
scope: pg.text("scope"),
idToken: pg.text("id_token"),
password: pg.text("password"),
accessToken: pg.text("access_token"),
refreshToken: pg.text("refresh_token"),
accessTokenExpiresAt: pg.timestamp("access_token_expires_at", { withTimezone: true }),
refreshTokenExpiresAt: pg.timestamp("refresh_token_expires_at", { withTimezone: true }),
createdAt: pg.timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: pg
.timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()),
},
(t) => [pg.index().on(t.userId)],
);
export const verification = pg.pgTable(
"verification",
{
id: pg
.uuid("id")
.notNull()
.primaryKey()
.$defaultFn(() => generateId()),
identifier: pg.text("identifier").notNull().unique(),
value: pg.text("value").notNull(),
expiresAt: pg.timestamp("expires_at", { withTimezone: true }).notNull(),
createdAt: pg.timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: pg
.timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()),
},
(t) => [pg.index().on(t.identifier)],
);
export const twoFactor = pg.pgTable(
"two_factor",
{
id: pg
.uuid("id")
.notNull()
.primaryKey()
.$defaultFn(() => generateId()),
userId: pg
.uuid("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
secret: pg.text("secret"),
backupCodes: pg.text("backup_codes"),
createdAt: pg.timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: pg
.timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()),
},
(t) => [pg.index().on(t.userId), pg.index().on(t.secret)],
);
export const passkey = pg.pgTable(
"passkey",
{
id: pg
.uuid("id")
.notNull()
.primaryKey()
.$defaultFn(() => generateId()),
name: pg.text("name"),
aaguid: pg.text("aaguid"),
publicKey: pg.text("public_key").notNull(),
credentialID: pg.text("credential_id").notNull(),
counter: pg.integer("counter").notNull(),
deviceType: pg.text("device_type").notNull(),
backedUp: pg.boolean("backed_up").notNull().default(false),
transports: pg.text("transports").notNull(),
userId: pg
.uuid("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
createdAt: pg.timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: pg
.timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()),
},
(t) => [pg.index().on(t.userId)],
);
export const resume = pg.pgTable(
"resume",
{
id: pg
.uuid("id")
.notNull()
.primaryKey()
.$defaultFn(() => generateId()),
name: pg.text("name").notNull(),
slug: pg.text("slug").notNull(),
tags: pg.text("tags").array().notNull().default([]),
isPublic: pg.boolean("is_public").notNull().default(false),
isLocked: pg.boolean("is_locked").notNull().default(false),
password: pg.text("password"),
data: pg
.jsonb("data")
.notNull()
.$type<ResumeData>()
.$defaultFn(() => defaultResumeData),
userId: pg
.uuid("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
createdAt: pg.timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: pg
.timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()),
},
(t) => [
pg.unique().on(t.slug, t.userId),
pg.index().on(t.userId),
pg.index().on(t.userId, t.updatedAt.desc()),
pg.index().on(t.isPublic, t.slug, t.userId),
],
);
export const resumeStatistics = pg.pgTable(
"resume_statistics",
{
id: pg
.uuid("id")
.notNull()
.primaryKey()
.$defaultFn(() => generateId()),
views: pg.integer("views").notNull().default(0),
downloads: pg.integer("downloads").notNull().default(0),
lastViewedAt: pg.timestamp("last_viewed_at", { withTimezone: true }),
lastDownloadedAt: pg.timestamp("last_downloaded_at", { withTimezone: true }),
resumeId: pg
.uuid("resume_id")
.unique()
.notNull()
.references(() => resume.id, { onDelete: "cascade" }),
createdAt: pg.timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: pg
.timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()),
},
(t) => [pg.index().on(t.resumeId)],
);
export const apikey = pg.pgTable(
"apikey",
{
id: pg
.uuid("id")
.notNull()
.primaryKey()
.$defaultFn(() => generateId()),
name: pg.text("name"),
start: pg.text("start"),
prefix: pg.text("prefix"),
key: pg.text("key").notNull(),
userId: pg
.uuid("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
refillInterval: pg.integer("refill_interval"),
refillAmount: pg.integer("refill_amount"),
lastRefillAt: pg.timestamp("last_refill_at", { withTimezone: true }),
enabled: pg.boolean("enabled").notNull().default(true),
rateLimitEnabled: pg.boolean("rate_limit_enabled").notNull().default(false),
rateLimitTimeWindow: pg.integer("rate_limit_time_window"),
rateLimitMax: pg.integer("rate_limit_max"),
requestCount: pg.integer("request_count").notNull().default(0),
remaining: pg.integer("remaining"),
lastRequest: pg.timestamp("last_request", { withTimezone: true }),
expiresAt: pg.timestamp("expires_at", { withTimezone: true }),
createdAt: pg.timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: pg
.timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()),
permissions: pg.text("permissions"),
metadata: pg.jsonb("metadata"),
},
(t) => [pg.index().on(t.userId), pg.index().on(t.key), pg.index().on(t.enabled, t.userId)],
);
+61
View File
@@ -0,0 +1,61 @@
import nodemailer, { type Transporter } from "nodemailer";
import { env } from "@/utils/env";
type SendEmailOptions = {
to: string | string[];
subject: string;
text: string;
from?: string;
};
const isSmtpEnabled = () => {
return !!env.SMTP_HOST && !!env.SMTP_USER && !!env.SMTP_PASS && !!env.SMTP_FROM;
};
let cachedTransport: Transporter | undefined;
const getTransport = () => {
if (!isSmtpEnabled()) return;
if (cachedTransport) return cachedTransport;
cachedTransport = nodemailer.createTransport({
host: env.SMTP_HOST,
port: env.SMTP_PORT,
secure: env.SMTP_SECURE,
auth: {
// biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null
user: env.SMTP_USER!,
// biome-ignore lint/style/noNonNullAssertion: enabled check ensures these are not null
pass: env.SMTP_PASS!,
},
});
return cachedTransport;
};
export const sendEmail = async (options: SendEmailOptions) => {
const transport = getTransport();
const from = options.from ?? env.SMTP_FROM ?? "Reactive Resume <noreply@localhost>";
const payload: nodemailer.SendMailOptions = {
to: options.to,
from,
subject: options.subject,
text: options.text,
};
if (!transport) return console.log("[EMAIL] SMTP not configured; logging email:", payload);
try {
await transport.sendMail({ ...options, from });
} catch (error) {
console.error(
"[EMAIL] Failed to send via SMTP; logging email payload instead:",
{
smtp: { host: env.SMTP_HOST, port: env.SMTP_PORT, secure: env.SMTP_SECURE },
email: payload,
},
error,
);
}
};
+625
View File
@@ -0,0 +1,625 @@
import { flattenError, ZodError, z } from "zod";
import type { IconName } from "@/schema/icons";
import { defaultResumeData, type ResumeData, resumeDataSchema } from "@/schema/resume/data";
import { generateId } from "@/utils/string";
// Custom ISO 8601 date pattern that allows partial dates (year only, year-month, or full date)
const iso8601 = z
.string()
.regex(
/^([1-2][0-9]{3}-[0-1][0-9]-[0-3][0-9]|[1-2][0-9]{3}-[0-1][0-9]|[1-2][0-9]{3})$/,
"Must be a valid ISO 8601 date (YYYY, YYYY-MM, or YYYY-MM-DD)",
);
const locationSchema = z.looseObject({
address: z.string().optional(),
postalCode: z.string().optional(),
city: z.string().optional(),
countryCode: z.string().optional(),
region: z.string().optional(),
});
const profileSchema = z.looseObject({
network: z.string().optional(),
username: z.string().optional(),
url: z.url().optional(),
});
const basicsSchema = z.looseObject({
name: z.string().optional(),
label: z.string().optional(),
image: z.string().optional(),
email: z.email().optional(),
phone: z.string().optional(),
url: z.url().optional(),
summary: z.string().optional(),
location: locationSchema.optional(),
profiles: z.array(profileSchema).optional(),
});
const workSchema = z.looseObject({
name: z.string().optional(),
location: z.string().optional(),
description: z.string().optional(),
position: z.string().optional(),
url: z.url().optional(),
startDate: iso8601.optional(),
endDate: iso8601.optional(),
summary: z.string().optional(),
highlights: z.array(z.string()).optional(),
});
const volunteerSchema = z.looseObject({
organization: z.string().optional(),
position: z.string().optional(),
url: z.url().optional(),
startDate: iso8601.optional(),
endDate: iso8601.optional(),
summary: z.string().optional(),
highlights: z.array(z.string()).optional(),
});
const educationSchema = z.looseObject({
institution: z.string().optional(),
url: z.url().optional(),
area: z.string().optional(),
studyType: z.string().optional(),
startDate: iso8601.optional(),
endDate: iso8601.optional(),
score: z.string().optional(),
courses: z.array(z.string()).optional(),
});
const awardSchema = z.looseObject({
title: z.string().optional(),
date: iso8601.optional(),
awarder: z.string().optional(),
summary: z.string().optional(),
});
const certificateSchema = z.looseObject({
name: z.string().optional(),
date: iso8601.optional(),
url: z.url().optional(),
issuer: z.string().optional(),
});
const publicationSchema = z.looseObject({
name: z.string().optional(),
publisher: z.string().optional(),
releaseDate: iso8601.optional(),
url: z.url().optional(),
summary: z.string().optional(),
});
const skillSchema = z.looseObject({
name: z.string().optional(),
level: z.string().optional(),
keywords: z.array(z.string()).optional(),
});
const languageSchema = z.looseObject({
language: z.string().optional(),
fluency: z.string().optional(),
});
const interestSchema = z.looseObject({
name: z.string().optional(),
keywords: z.array(z.string()).optional(),
});
const referenceSchema = z.looseObject({
name: z.string().optional(),
reference: z.string().optional(),
});
const projectSchema = z.looseObject({
name: z.string().optional(),
description: z.string().optional(),
highlights: z.array(z.string()).optional(),
keywords: z.array(z.string()).optional(),
startDate: iso8601.optional(),
endDate: iso8601.optional(),
url: z.url().optional(),
roles: z.array(z.string()).optional(),
entity: z.string().optional(),
type: z.string().optional(),
});
const metaSchema = z.looseObject({
canonical: z.url().optional(),
version: z.string().optional(),
lastModified: z.string().optional(),
});
const jsonResumeSchema = z.looseObject({
$schema: z.url().optional(),
basics: basicsSchema.optional(),
work: z.array(workSchema).optional(),
volunteer: z.array(volunteerSchema).optional(),
education: z.array(educationSchema).optional(),
awards: z.array(awardSchema).optional(),
certificates: z.array(certificateSchema).optional(),
publications: z.array(publicationSchema).optional(),
skills: z.array(skillSchema).optional(),
languages: z.array(languageSchema).optional(),
interests: z.array(interestSchema).optional(),
references: z.array(referenceSchema).optional(),
projects: z.array(projectSchema).optional(),
meta: metaSchema.optional(),
});
type JSONResume = z.infer<typeof jsonResumeSchema>;
// Helper function to format date period from start and end dates
function formatPeriod(startDate?: string, endDate?: string): string {
if (!startDate && !endDate) return "";
if (!startDate) return endDate || "";
if (!endDate) return `${startDate} - Present`;
// Format dates to be more readable
const formatDate = (date: string): string => {
// Handle YYYY-MM-DD, YYYY-MM, or YYYY formats
const parts = date.split("-");
if (parts.length === 3) {
// YYYY-MM-DD
const [year, month] = parts;
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
return `${monthNames[parseInt(month, 10) - 1]} ${year}`;
}
if (parts.length === 2) {
// YYYY-MM
const [year, month] = parts;
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
return `${monthNames[parseInt(month, 10) - 1]} ${year}`;
}
// YYYY
return date;
};
return `${formatDate(startDate)} - ${formatDate(endDate)}`;
}
// Helper function to format a single date
function formatSingleDate(date?: string): string {
if (!date) return "";
// Format dates to be more readable
const parts = date.split("-");
if (parts.length === 3) {
// YYYY-MM-DD
const [year, month, day] = parts;
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
return `${monthNames[parseInt(month, 10) - 1]} ${day}, ${year}`;
}
if (parts.length === 2) {
// YYYY-MM
const [year, month] = parts;
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
return `${monthNames[parseInt(month, 10) - 1]} ${year}`;
}
// YYYY
return date;
}
// Helper function to convert text and highlights to HTML
function toHtmlDescription(summary?: string, highlights?: string[]): string {
const parts: string[] = [];
if (summary) {
parts.push(`<p>${summary}</p>`);
}
if (highlights && highlights.length > 0) {
parts.push("<ul>");
for (const highlight of highlights) {
parts.push(`<li>${highlight}</li>`);
}
parts.push("</ul>");
}
return parts.join("");
}
// Helper function to convert array to HTML list
function arrayToHtmlList(items?: string[]): string {
if (!items || items.length === 0) return "";
return `<ul>${items.map((item) => `<li>${item}</li>`).join("")}</ul>`;
}
// Helper function to format location object to string
function formatLocation(location?: {
address?: string;
postalCode?: string;
city?: string;
countryCode?: string;
region?: string;
}): string {
if (!location) return "";
const parts: string[] = [];
if (location.city) parts.push(location.city);
if (location.region) parts.push(location.region);
if (location.countryCode) parts.push(location.countryCode);
return parts.join(", ");
}
// Helper function to map network name to icon
function getNetworkIcon(network?: string): IconName {
if (!network) return "star";
const networkLower = network.toLowerCase();
if (networkLower.includes("github")) return "github-logo";
if (networkLower.includes("linkedin")) return "linkedin-logo";
if (networkLower.includes("twitter") || networkLower.includes("x.com")) return "twitter-logo";
if (networkLower.includes("facebook")) return "facebook-logo";
if (networkLower.includes("instagram")) return "instagram-logo";
if (networkLower.includes("youtube")) return "youtube-logo";
if (networkLower.includes("stackoverflow") || networkLower.includes("stack-overflow")) return "stack-overflow-logo";
if (networkLower.includes("medium")) return "medium-logo";
if (networkLower.includes("dev.to") || networkLower.includes("devto")) return "code";
if (networkLower.includes("dribbble")) return "dribbble-logo";
if (networkLower.includes("behance")) return "behance-logo";
if (networkLower.includes("gitlab")) return "git-branch";
if (networkLower.includes("bitbucket")) return "code";
if (networkLower.includes("codepen")) return "code";
return "star";
}
// Helper function to parse skill/language level to number (0-5)
function parseLevel(level?: string): number {
if (!level) return 0;
const levelLower = level.toLowerCase();
// Try to parse numeric values
const numeric = parseInt(levelLower, 10);
if (!Number.isNaN(numeric) && numeric >= 0 && numeric <= 5) return numeric;
// Map text levels to numbers
if (levelLower.includes("native") || levelLower.includes("expert") || levelLower.includes("master")) return 5;
if (levelLower.includes("fluent") || levelLower.includes("advanced") || levelLower.includes("proficient")) return 4;
if (levelLower.includes("intermediate") || levelLower.includes("conversational")) return 3;
if (levelLower.includes("beginner") || levelLower.includes("basic") || levelLower.includes("elementary")) return 2;
if (levelLower.includes("novice")) return 1;
// CEFR levels
if (levelLower.includes("c2")) return 5;
if (levelLower.includes("c1")) return 4;
if (levelLower.includes("b2")) return 3;
if (levelLower.includes("b1")) return 2;
if (levelLower.includes("a2")) return 1;
if (levelLower.includes("a1")) return 1;
return 0;
}
// Helper function to create URL object
function createUrl(url?: string, label?: string): { url: string; label: string } {
if (!url) return { url: "", label: "" };
return { url, label: label || url };
}
export class JSONResumeImporter {
convert(jsonResume: JSONResume): ResumeData {
const result: ResumeData = {
...defaultResumeData,
};
// Map basics
if (jsonResume.basics) {
const basics = jsonResume.basics;
result.basics = {
name: basics.name || "",
headline: basics.label || "",
email: basics.email || "",
phone: basics.phone || "",
location: basics.location ? formatLocation(basics.location) : "",
website: createUrl(basics.url),
customFields: [],
};
// Map image to picture
if (basics.image) {
result.picture = {
...defaultResumeData.picture,
url: basics.image,
hidden: false,
};
}
}
// Map summary
if (jsonResume.basics?.summary) {
result.summary = {
...defaultResumeData.summary,
content: `<p>${jsonResume.basics.summary}</p>`,
hidden: false,
};
}
// Map work to experience
if (jsonResume.work && jsonResume.work.length > 0) {
result.sections.experience = {
...defaultResumeData.sections.experience,
items: jsonResume.work
.filter((work) => work.name || work.position)
.map((work) => ({
id: generateId(),
hidden: false,
company: work.name || "",
position: work.position || "",
location: work.location || "",
period: formatPeriod(work.startDate, work.endDate),
website: createUrl(work.url),
description: toHtmlDescription(work.summary, work.highlights),
})),
};
}
// Map education
if (jsonResume.education && jsonResume.education.length > 0) {
result.sections.education = {
...defaultResumeData.sections.education,
items: jsonResume.education
.filter((edu) => edu.institution)
.map((edu) => ({
id: generateId(),
hidden: false,
school: edu.institution || "",
degree: [edu.studyType, edu.area].filter(Boolean).join(" in ") || "",
area: edu.area || "",
grade: edu.score || "",
location: "",
period: formatPeriod(edu.startDate, edu.endDate),
website: createUrl(edu.url),
description: edu.courses && edu.courses.length > 0 ? arrayToHtmlList(edu.courses) : "",
})),
};
}
// Map projects
if (jsonResume.projects && jsonResume.projects.length > 0) {
result.sections.projects = {
...defaultResumeData.sections.projects,
items: jsonResume.projects
.filter((project) => project.name)
.map((project) => ({
id: generateId(),
hidden: false,
name: project.name || "",
period: formatPeriod(project.startDate, project.endDate),
website: createUrl(project.url),
description: toHtmlDescription(project.description, project.highlights),
})),
};
}
// Map skills
if (jsonResume.skills && jsonResume.skills.length > 0) {
result.sections.skills = {
...defaultResumeData.sections.skills,
items: jsonResume.skills
.filter((skill) => skill.name)
.map((skill) => ({
id: generateId(),
hidden: false,
icon: "star",
name: skill.name || "",
proficiency: skill.level || "",
level: parseLevel(skill.level),
keywords: skill.keywords || [],
})),
};
}
// Map languages
if (jsonResume.languages && jsonResume.languages.length > 0) {
result.sections.languages = {
...defaultResumeData.sections.languages,
items: jsonResume.languages
.filter((lang) => lang.language)
.map((lang) => ({
id: generateId(),
hidden: false,
language: lang.language || "",
fluency: lang.fluency || "",
level: parseLevel(lang.fluency),
})),
};
}
// Map interests
if (jsonResume.interests && jsonResume.interests.length > 0) {
result.sections.interests = {
...defaultResumeData.sections.interests,
items: jsonResume.interests
.filter((interest) => interest.name)
.map((interest) => ({
id: generateId(),
hidden: false,
icon: "star",
name: interest.name || "",
keywords: interest.keywords || [],
})),
};
}
// Map awards
if (jsonResume.awards && jsonResume.awards.length > 0) {
result.sections.awards = {
...defaultResumeData.sections.awards,
items: jsonResume.awards
.filter((award) => award.title)
.map((award) => ({
id: generateId(),
hidden: false,
title: award.title || "",
awarder: award.awarder || "",
date: formatSingleDate(award.date),
website: { url: "", label: "" },
description: award.summary ? `<p>${award.summary}</p>` : "",
})),
};
}
// Map certificates
if (jsonResume.certificates && jsonResume.certificates.length > 0) {
result.sections.certifications = {
...defaultResumeData.sections.certifications,
items: jsonResume.certificates
.filter((cert) => cert.name)
.map((cert) => ({
id: generateId(),
hidden: false,
title: cert.name || "",
issuer: cert.issuer || "",
date: formatSingleDate(cert.date),
website: createUrl(cert.url),
description: "",
})),
};
}
// Map publications
if (jsonResume.publications && jsonResume.publications.length > 0) {
result.sections.publications = {
...defaultResumeData.sections.publications,
items: jsonResume.publications
.filter((pub) => pub.name)
.map((pub) => ({
id: generateId(),
hidden: false,
title: pub.name || "",
publisher: pub.publisher || "",
date: formatSingleDate(pub.releaseDate),
website: createUrl(pub.url),
description: pub.summary ? `<p>${pub.summary}</p>` : "",
})),
};
}
// Map volunteer
if (jsonResume.volunteer && jsonResume.volunteer.length > 0) {
result.sections.volunteer = {
...defaultResumeData.sections.volunteer,
items: jsonResume.volunteer
.filter((vol) => vol.organization)
.map((vol) => ({
id: generateId(),
hidden: false,
organization: vol.organization || "",
location: "",
period: formatPeriod(vol.startDate, vol.endDate),
website: createUrl(vol.url),
description: toHtmlDescription(vol.summary, vol.highlights),
})),
};
}
// Map references
if (jsonResume.references && jsonResume.references.length > 0) {
result.sections.references = {
...defaultResumeData.sections.references,
items: jsonResume.references
.filter((ref) => ref.name || ref.reference)
.map((ref) => ({
id: generateId(),
hidden: false,
name: ref.name || "",
description: ref.reference ? `<p>${ref.reference}</p>` : "",
})),
};
}
// Map profiles (from basics.profiles) to profiles section
if (jsonResume.basics?.profiles && jsonResume.basics.profiles.length > 0) {
result.sections.profiles = {
...defaultResumeData.sections.profiles,
items: jsonResume.basics.profiles
.filter((profile) => profile.network)
.map((profile) => ({
id: generateId(),
hidden: false,
icon: getNetworkIcon(profile.network),
network: profile.network || "",
username: profile.username || "",
website: createUrl(profile.url, profile.username || profile.network),
})),
};
}
return resumeDataSchema.parse(result);
}
parse(json: string): ResumeData {
try {
const jsonResume = jsonResumeSchema.parse(JSON.parse(json));
return this.convert(jsonResume);
} catch (error) {
if (error instanceof ZodError) {
const errors = flattenError(error);
throw new Error(JSON.stringify(errors));
}
throw error;
}
}
}
@@ -0,0 +1,17 @@
import { flattenError, ZodError } from "zod";
import { type ResumeData, resumeDataSchema } from "@/schema/resume/data";
export class ReactiveResumeJSONImporter {
parse(json: string): ResumeData {
try {
return resumeDataSchema.parse(JSON.parse(json));
} catch (error) {
if (error instanceof ZodError) {
const errors = flattenError(error);
throw new Error(JSON.stringify(errors));
}
throw error;
}
}
}
@@ -0,0 +1,769 @@
import z, { flattenError, ZodError } from "zod";
import { type ResumeData, resumeDataSchema } from "@/schema/resume/data";
import { type Template, templateSchema } from "@/schema/templates";
import { parseRgbString } from "@/utils/color";
import { generateId } from "@/utils/string";
function colorToRgba(color: string): string {
const parsed = parseRgbString(color);
if (!parsed) return "rgba(0, 0, 0, 1)";
return `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${parsed.a})`;
}
const clamp = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value));
const nonNegative = (value: number): number => Math.max(0, value);
const pxToPt = (px: number): number => px * 0.75;
const clampPictureSize = (size: number): number => clamp(size, 32, 512);
const clampRotation = (rotation: number): number => clamp(rotation, 0, 360);
const clampAspectRatio = (ratio: number): number => clamp(ratio, 0.5, 2.5);
const clampBorderRadius = (radius: number): number => clamp(radius, 0, 100);
const clampFontSize = (size: number): number => clamp(size, 6, 24);
const clampLineHeight = (height: number): number => clamp(height, 0.5, 4);
const clampSidebarWidth = (width: number): number => clamp(width, 10, 50);
const clampLevel = (level: number): number => clamp(level, 0, 5);
const convertAndClampFontSize = (px: number): number => clampFontSize(pxToPt(px));
const isValidEmail = (email: string): boolean => {
if (!email) return false;
return z.email().safeParse(email).success;
};
const sanitizeEmail = (email: string | undefined): string => {
if (!email) return "";
return isValidEmail(email) ? email : "";
};
type FontWeight = "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900";
const FONT_WEIGHT_MAP: Record<string, FontWeight> = {
regular: "400",
italic: "400",
"100": "100",
"200": "200",
"300": "300",
"400": "400",
"500": "500",
"600": "600",
"700": "700",
"800": "800",
"900": "900",
bold: "700",
"bold-italic": "700",
};
const convertFontVariantToWeight = (variant: string, defaultWeight: FontWeight = "400"): FontWeight =>
FONT_WEIGHT_MAP[variant.toLowerCase()] ?? defaultWeight;
const convertFontVariants = (variants: string[] | undefined, defaultWeight: FontWeight = "400"): FontWeight[] => {
if (!variants || variants.length === 0) return [defaultWeight];
return variants.map((v) => convertFontVariantToWeight(v, defaultWeight));
};
const convertFontVariantsForHeading = (variants: string[] | undefined): FontWeight[] => {
const weights = convertFontVariants(variants, "600");
const filtered = weights.filter((w) => Number.parseInt(w, 10) >= 600);
return filtered.length > 0 ? filtered : ["600"];
};
type V4ResumeData = {
basics: {
name: string;
headline: string;
email: string;
phone: string;
location: string;
url: {
label: string;
href: string;
};
customFields: Array<{
id?: string;
icon?: string;
text?: string;
}>;
picture: {
url: string;
size: number;
aspectRatio: number;
borderRadius: number;
effects: {
hidden: boolean;
border: boolean;
grayscale: boolean;
};
};
};
sections: {
summary: {
name: string;
columns: number;
separateLinks: boolean;
visible: boolean;
id: string;
content: string;
};
awards: {
name: string;
columns: number;
separateLinks: boolean;
visible: boolean;
id: string;
items: Array<{
id: string;
visible: boolean;
name?: string;
title?: string;
awarder?: string;
date?: string;
summary?: string;
url?: {
label: string;
href: string;
};
}>;
};
certifications: {
name: string;
columns: number;
separateLinks: boolean;
visible: boolean;
id: string;
items: Array<{
id: string;
visible: boolean;
name?: string;
issuer?: string;
date?: string;
summary?: string;
url?: {
label: string;
href: string;
};
}>;
};
education: {
name: string;
columns: number;
separateLinks: boolean;
visible: boolean;
id: string;
items: Array<{
id: string;
visible: boolean;
institution?: string;
studyType?: string;
area?: string;
score?: string;
date?: string;
summary?: string;
url?: {
label: string;
href: string;
};
}>;
};
experience: {
name: string;
columns: number;
separateLinks: boolean;
visible: boolean;
id: string;
items: Array<{
id: string;
visible: boolean;
company?: string;
position?: string;
location?: string;
date?: string;
summary?: string;
url?: {
label: string;
href: string;
};
}>;
};
volunteer: {
name: string;
columns: number;
separateLinks: boolean;
visible: boolean;
id: string;
items: Array<{
id: string;
visible: boolean;
organization?: string;
position?: string;
location?: string;
date?: string;
summary?: string;
url?: {
label: string;
href: string;
};
}>;
};
interests: {
name: string;
columns: number;
separateLinks: boolean;
visible: boolean;
id: string;
items: Array<{
id: string;
visible: boolean;
name?: string;
keywords?: string[];
}>;
};
languages: {
name: string;
columns: number;
separateLinks: boolean;
visible: boolean;
id: string;
items: Array<{
id: string;
visible: boolean;
language?: string;
fluency?: string;
level?: number;
}>;
};
profiles: {
name: string;
columns: number;
separateLinks: boolean;
visible: boolean;
id: string;
items: Array<{
id: string;
visible: boolean;
network?: string;
username?: string;
icon?: string;
url?: {
label: string;
href: string;
};
}>;
};
projects: {
name: string;
columns: number;
separateLinks: boolean;
visible: boolean;
id: string;
items: Array<{
id: string;
visible: boolean;
name?: string;
description?: string;
date?: string;
summary?: string;
keywords?: string[];
url?: {
label: string;
href: string;
};
}>;
};
publications: {
name: string;
columns: number;
separateLinks: boolean;
visible: boolean;
id: string;
items: Array<{
id: string;
visible: boolean;
name?: string;
publisher?: string;
date?: string;
summary?: string;
url?: {
label: string;
href: string;
};
}>;
};
references: {
name: string;
columns: number;
separateLinks: boolean;
visible: boolean;
id: string;
items: Array<{
id: string;
visible: boolean;
name?: string;
description?: string;
summary?: string;
url?: {
label: string;
href: string;
};
}>;
};
skills: {
name: string;
columns: number;
separateLinks: boolean;
visible: boolean;
id: string;
items: Array<{
id: string;
visible: boolean;
name?: string;
description?: string;
level?: number;
keywords?: string[];
}>;
};
custom?: Record<
string,
{
name: string;
columns: number;
separateLinks: boolean;
visible: boolean;
id: string;
items: Array<{
id: string;
visible: boolean;
name?: string;
description?: string;
date?: string;
location?: string;
summary?: string;
keywords?: string[];
url?: {
label: string;
href: string;
};
}>;
}
>;
};
metadata: {
template: string;
layout: Array<Array<string[]>>;
css: {
value: string;
visible: boolean;
};
page: {
margin: number;
format: "a4" | "letter";
options: {
breakLine: boolean;
pageNumbers: boolean;
};
};
theme: {
background: string;
text: string;
primary: string;
};
typography: {
font: {
family: string;
subset: string;
variants: string[];
size: number;
};
lineHeight: number;
hideIcons: boolean;
underlineLinks: boolean;
};
notes: string;
};
};
export class ReactiveResumeV4JSONImporter {
parse(json: string): ResumeData {
try {
const v4Data = JSON.parse(json) as V4ResumeData;
const transformed: ResumeData = {
picture: {
hidden: v4Data.basics.picture?.effects?.hidden ?? false,
url: v4Data.basics.picture?.url ?? "",
size: clampPictureSize(v4Data.basics.picture?.size ?? 80),
rotation: clampRotation(0),
aspectRatio: clampAspectRatio(v4Data.basics.picture?.aspectRatio ?? 1),
borderRadius: clampBorderRadius(v4Data.basics.picture?.borderRadius ?? 0),
borderColor: v4Data.basics.picture?.effects?.border ? "rgba(0, 0, 0, 0.5)" : "rgba(0, 0, 0, 0)",
borderWidth: nonNegative(v4Data.basics.picture?.effects?.border ? 1 : 0),
shadowColor: "rgba(0, 0, 0, 0.5)",
shadowWidth: nonNegative(0),
},
basics: {
name: v4Data.basics.name ?? "",
headline: v4Data.basics.headline ?? "",
email: sanitizeEmail(v4Data.basics.email),
phone: v4Data.basics.phone ?? "",
location: v4Data.basics.location ?? "",
website: {
url: v4Data.basics.url?.href ?? "",
label: v4Data.basics.url?.label ?? "",
},
customFields: (v4Data.basics.customFields ?? []).map((field) => ({
id: field.id ?? generateId(),
icon: field.icon ?? "",
text: field.text ?? "",
})),
},
summary: {
title: v4Data.sections.summary?.name ?? "",
columns: v4Data.sections.summary?.columns ?? 1,
hidden: !(v4Data.sections.summary?.visible ?? true),
content: v4Data.sections.summary?.content ?? "",
},
sections: {
profiles: {
title: (v4Data.sections.profiles?.name ?? "").trim() || "Profiles",
columns: v4Data.sections.profiles?.columns ?? 1,
hidden: !(v4Data.sections.profiles?.visible ?? true),
items: (v4Data.sections.profiles?.items ?? [])
.filter((item) => item.network && item.network.trim().length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
icon: item.icon ?? "",
network: item.network?.trim() || "Other",
username: item.username ?? "",
website: {
url: item.url?.href ?? "",
label: item.url?.label ?? "",
},
})),
},
experience: {
title: (v4Data.sections.experience?.name ?? "").trim() || "Experience",
columns: v4Data.sections.experience?.columns ?? 1,
hidden: !(v4Data.sections.experience?.visible ?? true),
items: (v4Data.sections.experience?.items ?? [])
.filter((item) => item.company && item.company.trim().length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
company: item.company?.trim() || "Company",
position: item.position ?? "",
location: item.location ?? "",
period: item.date ?? "",
website: {
url: item.url?.href ?? "",
label: item.url?.label ?? "",
},
description: item.summary ?? "",
})),
},
education: {
title: (v4Data.sections.education?.name ?? "").trim() || "Education",
columns: v4Data.sections.education?.columns ?? 1,
hidden: !(v4Data.sections.education?.visible ?? true),
items: (v4Data.sections.education?.items ?? [])
.filter((item) => item.institution && item.institution.trim().length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
school: item.institution?.trim() || "School",
degree: item.studyType ?? "",
area: item.area ?? "",
grade: item.score ?? "",
location: "",
period: item.date ?? "",
website: {
url: item.url?.href ?? "",
label: item.url?.label ?? "",
},
description: item.summary ?? "",
})),
},
projects: {
title: (v4Data.sections.projects?.name ?? "").trim() || "Projects",
columns: v4Data.sections.projects?.columns ?? 1,
hidden: !(v4Data.sections.projects?.visible ?? true),
items: (v4Data.sections.projects?.items ?? [])
.filter((item) => item.name && item.name.trim().length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
name: item.name?.trim() || "Project",
period: item.date ?? "",
website: {
url: item.url?.href ?? "",
label: item.url?.label ?? "",
},
description: item.summary ?? item.description ?? "",
})),
},
skills: {
title: (v4Data.sections.skills?.name ?? "").trim() || "Skills",
columns: v4Data.sections.skills?.columns ?? 1,
hidden: !(v4Data.sections.skills?.visible ?? true),
items: (v4Data.sections.skills?.items ?? [])
.filter((item) => item.name && item.name.trim().length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
icon: "",
name: item.name?.trim() || "Skill",
proficiency: item.description ?? "",
level: clampLevel(item.level ?? 0),
keywords: item.keywords ?? [],
})),
},
languages: {
title: (v4Data.sections.languages?.name ?? "").trim() || "Languages",
columns: v4Data.sections.languages?.columns ?? 1,
hidden: !(v4Data.sections.languages?.visible ?? true),
items: (v4Data.sections.languages?.items ?? [])
.filter((item) => item.language && item.language.trim().length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
language: item.language?.trim() || "Language",
fluency: item.fluency ?? "",
level: clampLevel(item.level ?? 0),
})),
},
interests: {
title: (v4Data.sections.interests?.name ?? "").trim() || "Interests",
columns: v4Data.sections.interests?.columns ?? 1,
hidden: !(v4Data.sections.interests?.visible ?? true),
items: (v4Data.sections.interests?.items ?? [])
.filter((item) => item.name && item.name.trim().length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
icon: "",
name: item.name?.trim() || "Interest",
keywords: item.keywords ?? [],
})),
},
awards: {
title: (v4Data.sections.awards?.name ?? "").trim() || "Awards",
columns: v4Data.sections.awards?.columns ?? 1,
hidden: !(v4Data.sections.awards?.visible ?? true),
items: (v4Data.sections.awards?.items ?? [])
.filter((item) => (item.title || item.name) && (item.title?.trim() || item.name?.trim() || "").length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
title: (item.title || item.name)?.trim() || "Award",
awarder: item.awarder ?? "",
date: item.date ?? "",
website: {
url: item.url?.href ?? "",
label: item.url?.label ?? "",
},
description: item.summary ?? "",
})),
},
certifications: {
title: (v4Data.sections.certifications?.name ?? "").trim() || "Certifications",
columns: v4Data.sections.certifications?.columns ?? 1,
hidden: !(v4Data.sections.certifications?.visible ?? true),
items: (v4Data.sections.certifications?.items ?? [])
.filter((item) => item.name && item.name.trim().length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
title: item.name?.trim() || "Certification",
issuer: item.issuer ?? "",
date: item.date ?? "",
website: {
url: item.url?.href ?? "",
label: item.url?.label ?? "",
},
description: item.summary ?? "",
})),
},
publications: {
title: (v4Data.sections.publications?.name ?? "").trim() || "Publications",
columns: v4Data.sections.publications?.columns ?? 1,
hidden: !(v4Data.sections.publications?.visible ?? true),
items: (v4Data.sections.publications?.items ?? [])
.filter((item) => item.name && item.name.trim().length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
title: item.name?.trim() || "Publication",
publisher: item.publisher ?? "",
date: item.date ?? "",
website: {
url: item.url?.href ?? "",
label: item.url?.label ?? "",
},
description: item.summary ?? "",
})),
},
volunteer: {
title: (v4Data.sections.volunteer?.name ?? "").trim() || "Volunteering",
columns: v4Data.sections.volunteer?.columns ?? 1,
hidden: !(v4Data.sections.volunteer?.visible ?? true),
items: (v4Data.sections.volunteer?.items ?? [])
.filter((item) => item.organization && item.organization.trim().length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
organization: item.organization?.trim() || "Organization",
location: item.location ?? "",
period: item.date ?? "",
website: {
url: item.url?.href ?? "",
label: item.url?.label ?? "",
},
description: item.summary ?? "",
})),
},
references: {
title: (v4Data.sections.references?.name ?? "").trim() || "References",
columns: v4Data.sections.references?.columns ?? 1,
hidden: !(v4Data.sections.references?.visible ?? true),
items: (v4Data.sections.references?.items ?? [])
.filter((item) => item.name && item.name.trim().length > 0)
.map((item) => ({
id: item.id ?? generateId(),
hidden: !(item.visible ?? true),
name: item.name?.trim() || "Reference",
description: item.summary ?? item.description ?? "",
})),
},
},
customSections: Object.entries(v4Data.sections.custom ?? {}).map(([sectionId, section]) => {
const itemsHtml = section.items
.filter((item) => item.visible !== false)
.map((item) => {
const parts: string[] = [];
if (item.name) {
parts.push(`<h3>${item.name}</h3>`);
}
if (item.description) {
parts.push(`<p>${item.description}</p>`);
}
if (item.date || item.location) {
const details = [item.date, item.location].filter(Boolean).join(" • ");
if (details) parts.push(`<p><em>${details}</em></p>`);
}
if (item.summary) {
parts.push(`<div>${item.summary}</div>`);
}
if (item.keywords && item.keywords.length > 0) {
parts.push(`<p><strong>Keywords:</strong> ${item.keywords.join(", ")}</p>`);
}
if (item.url?.href) {
const label = item.url.label || item.url.href;
parts.push(`<p><a href="${item.url.href}">${label}</a></p>`);
}
return parts.length > 0 ? `<div style="margin-bottom: 1em;">${parts.join("")}</div>` : "";
})
.filter(Boolean)
.join("");
return {
id: section.id || sectionId,
title: section.name ?? "",
columns: section.columns ?? 1,
hidden: !(section.visible ?? true),
content: itemsHtml || "",
};
}),
metadata: {
template: (templateSchema.safeParse(v4Data.metadata.template).success
? v4Data.metadata.template
: "onyx") as Template,
layout: {
sidebarWidth: clampSidebarWidth(35),
pages: (v4Data.metadata.layout ?? []).map((page) => {
const main = page[0] ?? [];
const sidebar = page[1] ?? [];
return {
fullWidth: sidebar.length === 0,
main: main.filter((id) => id !== "summary"),
sidebar: sidebar,
};
}),
},
css: {
enabled: v4Data.metadata.css?.visible ?? false,
value: v4Data.metadata.css?.value ?? "",
},
page: {
gapX: nonNegative(4),
gapY: nonNegative(6),
marginX: nonNegative(v4Data.metadata.page?.margin ?? 14),
marginY: nonNegative(v4Data.metadata.page?.margin ?? 14),
format: v4Data.metadata.page?.format ?? "a4",
locale: "en-US",
hideIcons: v4Data.metadata.typography?.hideIcons ?? false,
},
design: {
colors: {
primary: v4Data.metadata.theme?.primary
? colorToRgba(v4Data.metadata.theme.primary)
: "rgba(220, 38, 38, 1)",
text: v4Data.metadata.theme?.text ? colorToRgba(v4Data.metadata.theme.text) : "rgba(0, 0, 0, 1)",
background: v4Data.metadata.theme?.background
? colorToRgba(v4Data.metadata.theme.background)
: "rgba(255, 255, 255, 1)",
},
level: {
icon: "star",
type: "circle",
},
},
typography: {
body: {
fontFamily: v4Data.metadata.typography?.font?.family ?? "IBM Plex Serif",
fontWeights: convertFontVariants(v4Data.metadata.typography?.font?.variants),
fontSize: convertAndClampFontSize(v4Data.metadata.typography?.font?.size ?? 14.67),
lineHeight: clampLineHeight(v4Data.metadata.typography?.lineHeight ?? 1.5),
},
heading: {
fontFamily: v4Data.metadata.typography?.font?.family ?? "IBM Plex Serif",
fontWeights: convertFontVariantsForHeading(v4Data.metadata.typography?.font?.variants),
fontSize: clampFontSize(convertAndClampFontSize(v4Data.metadata.typography?.font?.size ?? 14.67) + 3),
lineHeight: clampLineHeight(v4Data.metadata.typography?.lineHeight ?? 1.5),
},
},
notes: v4Data.metadata.notes ?? "",
},
};
if (v4Data.sections.summary?.visible && v4Data.sections.summary?.content) {
if (transformed.metadata.layout.pages.length > 0) {
transformed.metadata.layout.pages[0].main.unshift("summary");
}
}
return resumeDataSchema.parse(transformed);
} catch (error: unknown) {
if (error instanceof ZodError) {
const errors = flattenError(error);
throw new Error(JSON.stringify(errors));
}
throw error;
}
}
}
+54
View File
@@ -0,0 +1,54 @@
import { createORPCClient, onError } from "@orpc/client";
import { RPCLink } from "@orpc/client/fetch";
import { BatchLinkPlugin } from "@orpc/client/plugins";
import { createRouterClient, type InferRouterInputs, type InferRouterOutputs, type RouterClient } from "@orpc/server";
import { createTanstackQueryUtils } from "@orpc/tanstack-query";
import { createIsomorphicFn } from "@tanstack/react-start";
import { getRequestHeaders } from "@tanstack/react-start/server";
import router from "@/integrations/orpc/router";
import { getLocale } from "@/utils/locale";
export const getORPCClient = createIsomorphicFn()
.server((): RouterClient<typeof router> => {
return createRouterClient(router, {
interceptors: [
onError((error) => {
console.error(error);
}),
],
context: async () => {
const locale = await getLocale();
const reqHeaders = getRequestHeaders();
// Add a custom header to identify server-side calls
reqHeaders.set("x-server-side-call", "true");
return { locale, reqHeaders };
},
});
})
.client((): RouterClient<typeof router> => {
const link = new RPCLink({
url: `${window.location.origin}/api/rpc`,
fetch: (request, init) => {
return fetch(request, { ...init, credentials: "include" });
},
interceptors: [
onError((error) => {
if (error instanceof DOMException) return;
console.error(error);
}),
],
plugins: [new BatchLinkPlugin({ groups: [{ condition: () => true, context: {} }] })],
});
return createORPCClient(link);
});
export const client = getORPCClient();
export const orpc = createTanstackQueryUtils(client);
export type RouterInput = InferRouterInputs<typeof router>;
export type RouterOutput = InferRouterOutputs<typeof router>;
+86
View File
@@ -0,0 +1,86 @@
import { ORPCError, os } from "@orpc/server";
import type { User } from "better-auth";
import { eq } from "drizzle-orm";
import { env } from "@/utils/env";
import type { Locale } from "@/utils/locale";
import { auth } from "../auth/config";
import { db } from "../drizzle/client";
import { user } from "../drizzle/schema";
interface ORPCContext {
locale: Locale;
reqHeaders?: Headers;
}
async function getUserFromHeaders(headers: Headers): Promise<User | null> {
try {
const result = await auth.api.getSession({ headers });
if (!result || !result.user) return null;
return result.user;
} catch {
return null;
}
}
async function getUserFromApiKey(apiKey: string): Promise<User | null> {
try {
const result = await auth.api.verifyApiKey({ body: { key: apiKey } });
if (!result.key || !result.valid) return null;
const [userResult] = await db.select().from(user).where(eq(user.id, result.key.userId)).limit(1);
if (!userResult) return null;
return userResult;
} catch {
return null;
}
}
const base = os.$context<ORPCContext>();
export const publicProcedure = base.use(async ({ context, next }) => {
const headers = context.reqHeaders ?? new Headers();
const apiKey = headers.get("x-api-key");
const user = apiKey ? await getUserFromApiKey(apiKey) : await getUserFromHeaders(headers);
return next({
context: {
...context,
user: user ?? null,
},
});
});
export const protectedProcedure = publicProcedure.use(async ({ context, next }) => {
if (!context.user) throw new ORPCError("UNAUTHORIZED");
return next({
context: {
...context,
user: context.user,
},
});
});
/**
* Server-only procedure that can only be called from server-side code (e.g., loaders).
* Rejects requests from the browser with a 401 UNAUTHORIZED error.
*/
export const serverOnlyProcedure = publicProcedure.use(async ({ context, next }) => {
const headers = context.reqHeaders ?? new Headers();
// Check for the custom header that indicates this is a server-side call
// Server-side calls using createRouterClient have this header set
const isServerSideCall = env.FLAG_DEBUG_PRINTER || headers.get("x-server-side-call") === "true";
// If the header is not present, this is a client-side HTTP request - reject it
if (!isServerSideCall) {
throw new ORPCError("UNAUTHORIZED", {
message: "This endpoint can only be called from server-side code",
});
}
return next({ context });
});
@@ -0,0 +1,36 @@
import { createHash, timingSafeEqual } from "node:crypto";
import { getCookie, setCookie } from "@tanstack/react-start/server";
import { env } from "@/utils/env";
const RESUME_ACCESS_COOKIE_PREFIX = "resume_access";
const RESUME_ACCESS_TTL_SECONDS = 60 * 10; // 10 minutes
const getResumeAccessCookieName = (resumeId: string) => `${RESUME_ACCESS_COOKIE_PREFIX}_${resumeId}`;
const signResumeAccessToken = (resumeId: string, passwordHash: string): string =>
createHash("sha256").update(`${resumeId}:${passwordHash}`).digest("hex");
const safeEquals = (value: string, expected: string) => {
const valueBuffer = Buffer.from(value);
const expectedBuffer = Buffer.from(expected);
if (valueBuffer.length !== expectedBuffer.length) return false;
return timingSafeEqual(valueBuffer, expectedBuffer);
};
export const hasResumeAccess = (resumeId: string, passwordHash: string | null) => {
if (!passwordHash) return false;
const cookieName = getResumeAccessCookieName(resumeId);
const cookieValue = getCookie(cookieName);
if (!cookieValue) return false;
const expected = signResumeAccessToken(resumeId, passwordHash);
return safeEquals(cookieValue, expected);
};
export const grantResumeAccess = (resumeId: string, passwordHash: string) =>
setCookie(getResumeAccessCookieName(resumeId), signResumeAccessToken(resumeId, passwordHash), {
path: "/",
httpOnly: true,
sameSite: "lax",
maxAge: RESUME_ACCESS_TTL_SECONDS,
secure: env.APP_URL.startsWith("https"),
});
+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");
}),
};
+74
View File
@@ -0,0 +1,74 @@
import { ORPCError } from "@orpc/client";
import { and, eq, isNotNull } from "drizzle-orm";
import type { AuthProvider } from "@/integrations/auth/types";
import { schema } from "@/integrations/drizzle";
import { db } from "@/integrations/drizzle/client";
import { env } from "@/utils/env";
import { verifyPassword } from "@/utils/password";
import { grantResumeAccess } from "../helpers/resume-access";
import { getStorageService } from "./storage";
export type ProviderList = Partial<Record<AuthProvider, string>>;
const providers = {
list: (): ProviderList => {
const providers: ProviderList = { credential: "Password" };
if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) providers.google = "Google";
if (env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET) providers.github = "GitHub";
if (env.OAUTH_CLIENT_ID && env.OAUTH_CLIENT_SECRET) providers.custom = env.OAUTH_PROVIDER_NAME ?? "Custom OAuth";
return providers;
},
};
export const authService = {
providers,
verifyResumePassword: async (input: { slug: string; username: string; password: string }): Promise<boolean> => {
const [resume] = await db
.select({ id: schema.resume.id, password: schema.resume.password })
.from(schema.resume)
.innerJoin(schema.user, eq(schema.resume.userId, schema.user.id))
.where(
and(
isNotNull(schema.resume.password),
eq(schema.resume.slug, input.slug),
eq(schema.user.username, input.username),
),
);
if (!resume) throw new ORPCError("NOT_FOUND");
const passwordHash = resume.password as string;
const isValid = await verifyPassword(input.password, passwordHash);
if (!isValid) throw new ORPCError("INVALID_PASSWORD");
grantResumeAccess(resume.id, passwordHash);
return true;
},
deleteAccount: async (input: { userId: string }): Promise<void> => {
if (!input.userId || input.userId.length === 0) return;
const storageService = getStorageService();
// Delete all user files in one call (pictures, screenshots, pdfs)
// The storage service delete method supports recursive deletion via prefix
try {
await storageService.delete(`uploads/${input.userId}`);
} catch {
// Ignore error and proceed with deleting user
}
try {
await db.delete(schema.user).where(eq(schema.user.id, input.userId));
} catch (err) {
console.error(`Failed to delete user record for userId=${input.userId}:`, err);
throw new ORPCError("INTERNAL_SERVER_ERROR");
}
},
};
+172
View File
@@ -0,0 +1,172 @@
import { ORPCError } from "@orpc/server";
import { printMarginTemplates } from "@/schema/templates";
import { env } from "@/utils/env";
import { generatePrinterToken } from "@/utils/printer-token";
import { resumeService } from "./resume";
import { getStorageService, uploadFile } from "./storage";
const pageDimensions = {
a4: {
width: "210mm",
height: "297mm",
},
letter: {
width: "8.5in",
height: "11in",
},
} as const;
const SCREENSHOT_TTL = 1000 * 60 * 60; // 1 hour
export const printerService = {
printResumeAsPDF: async (input: { id: string; userId: string }): Promise<string> => {
const storageService = getStorageService();
// Delete any existing PDFs for this resume
const pdfPrefix = `uploads/${input.userId}/pdfs/${input.id}`;
await storageService.delete(pdfPrefix);
const resume = await resumeService.getByIdForPrinter({ id: input.id });
const format = resume.data.metadata.page.format;
const locale = resume.data.metadata.page.locale;
const baseUrl = env.PRINTER_APP_URL ?? env.APP_URL;
const domain = new URL(baseUrl).hostname;
const token = generatePrinterToken(input.id);
const url = `${baseUrl}/printer/${input.id}?token=${token}`;
const formData = new FormData();
const cookies = [{ name: "locale", value: locale, domain }];
const isPrintMargin = printMarginTemplates.includes(resume.data.metadata.template);
const marginX = isPrintMargin ? `${resume.data.metadata.page.marginX.toString()}pt` : "0";
const marginY = isPrintMargin ? `${resume.data.metadata.page.marginY.toString()}pt` : "0";
formData.append("url", url);
formData.append("marginTop", marginY);
formData.append("marginLeft", marginX);
formData.append("marginRight", marginX);
formData.append("marginBottom", marginY);
formData.append("printBackground", "true");
formData.append("skipNetworkIdleEvent", "false");
formData.append("cookies", JSON.stringify(cookies));
formData.append("paperWidth", pageDimensions[format].width);
formData.append("paperHeight", pageDimensions[format].height);
const headers = new Headers();
if (env.GOTENBERG_USERNAME && env.GOTENBERG_PASSWORD) {
const credentials = `${env.GOTENBERG_USERNAME}:${env.GOTENBERG_PASSWORD}`;
const encodedCredentials = btoa(credentials);
headers.set("Authorization", `Basic ${encodedCredentials}`);
}
const response = await fetch(`${env.GOTENBERG_ENDPOINT}/forms/chromium/convert/url`, {
headers,
method: "POST",
body: formData,
});
if (!response.ok) {
throw new ORPCError("UNAUTHORIZED", {
status: response.status,
message: response.statusText,
});
}
const pdfBuffer = await response.arrayBuffer();
// Store PDF and return URL
const result = await uploadFile({
userId: input.userId,
resumeId: input.id,
data: new Uint8Array(pdfBuffer),
contentType: "application/pdf",
type: "pdf",
});
return result.url;
},
getResumeScreenshot: async (input: { id: string; userId: string }): Promise<string> => {
const storageService = getStorageService();
const screenshotPrefix = `uploads/${input.userId}/screenshots/${input.id}`;
const existingScreenshots = await storageService.list(screenshotPrefix);
const now = Date.now();
if (existingScreenshots.length > 0) {
const sortedFiles = existingScreenshots
.map((path) => {
const filename = path.split("/").pop();
const match = filename?.match(/^(\d+)\.webp$/);
return match ? { path, timestamp: Number(match[1]) } : null;
})
.filter((item): item is { path: string; timestamp: number } => item !== null)
.sort((a, b) => b.timestamp - a.timestamp);
if (sortedFiles.length > 0) {
const latest = sortedFiles[0];
const age = now - latest.timestamp;
if (age < SCREENSHOT_TTL) {
// Return URL of cached screenshot
return new URL(latest.path, env.APP_URL).toString();
}
// Delete old screenshots
await Promise.all(sortedFiles.map((file) => storageService.delete(file.path)));
}
}
const baseUrl = env.PRINTER_APP_URL ?? env.APP_URL;
const token = generatePrinterToken(input.id);
const url = `${baseUrl}/printer/${input.id}?token=${token}`;
const formData = new FormData();
formData.append("url", url);
formData.append("clip", "true");
formData.append("width", "794");
formData.append("height", "1123");
formData.append("format", "webp");
formData.append("optimizeForSpeed", "true");
formData.append("skipNetworkIdleEvent", "false");
const headers = new Headers();
if (env.GOTENBERG_USERNAME && env.GOTENBERG_PASSWORD) {
const credentials = `${env.GOTENBERG_USERNAME}:${env.GOTENBERG_PASSWORD}`;
const encodedCredentials = btoa(credentials);
headers.set("Authorization", `Basic ${encodedCredentials}`);
}
const response = await fetch(`${env.GOTENBERG_ENDPOINT}/forms/chromium/screenshot/url`, {
headers,
method: "POST",
body: formData,
});
if (!response.ok) {
throw new ORPCError("UNAUTHORIZED", {
status: response.status,
message: response.statusText,
});
}
const imageBuffer = await response.arrayBuffer();
// Store screenshot and return URL
const result = await uploadFile({
userId: input.userId,
resumeId: input.id,
data: new Uint8Array(imageBuffer),
contentType: "image/webp",
type: "screenshot",
});
return result.url;
},
};
+308
View File
@@ -0,0 +1,308 @@
import { ORPCError } from "@orpc/client";
import { and, arrayContains, asc, desc, eq, sql } from "drizzle-orm";
import { match } from "ts-pattern";
import { schema } from "@/integrations/drizzle";
import { db } from "@/integrations/drizzle/client";
import type { ResumeData } from "@/schema/resume/data";
import { defaultResumeData } from "@/schema/resume/data";
import type { Locale } from "@/utils/locale";
import { hashPassword } from "@/utils/password";
import { generateId } from "@/utils/string";
import { hasResumeAccess } from "../helpers/resume-access";
import { getStorageService } from "./storage";
const tags = {
list: async (input: { userId: string }): Promise<string[]> => {
const result = await db
.select({ tags: schema.resume.tags })
.from(schema.resume)
.where(eq(schema.resume.userId, input.userId));
const uniqueTags = new Set(result.flatMap((tag) => tag.tags));
const sortedTags = Array.from(uniqueTags).sort((a, b) => a.localeCompare(b));
return sortedTags;
},
};
const statistics = {
getById: async (input: { id: string; userId: string }) => {
const [statistics] = await db
.select({
isPublic: schema.resume.isPublic,
views: schema.resumeStatistics.views,
downloads: schema.resumeStatistics.downloads,
lastViewedAt: schema.resumeStatistics.lastViewedAt,
lastDownloadedAt: schema.resumeStatistics.lastDownloadedAt,
})
.from(schema.resumeStatistics)
.rightJoin(schema.resume, eq(schema.resumeStatistics.resumeId, schema.resume.id))
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)));
return {
isPublic: statistics.isPublic,
views: statistics.views ?? 0,
downloads: statistics.downloads ?? 0,
lastViewedAt: statistics.lastViewedAt,
lastDownloadedAt: statistics.lastDownloadedAt,
};
},
increment: async (input: { id: string; views?: boolean; downloads?: boolean }): Promise<void> => {
const views = input.views ? 1 : 0;
const downloads = input.downloads ? 1 : 0;
const lastViewedAt = input.views ? sql`now()` : undefined;
const lastDownloadedAt = input.downloads ? sql`now()` : undefined;
await db
.insert(schema.resumeStatistics)
.values({
resumeId: input.id,
views,
downloads,
lastViewedAt,
lastDownloadedAt,
})
.onConflictDoUpdate({
target: [schema.resumeStatistics.resumeId],
set: {
views: sql`${schema.resumeStatistics.views} + ${views}`,
downloads: sql`${schema.resumeStatistics.downloads} + ${downloads}`,
lastViewedAt,
lastDownloadedAt,
},
});
},
};
export const resumeService = {
tags,
statistics,
list: async (input: { userId: string; tags: string[]; sort: "lastUpdatedAt" | "createdAt" | "name" }) => {
return await db
.select({
id: schema.resume.id,
name: schema.resume.name,
slug: schema.resume.slug,
tags: schema.resume.tags,
isPublic: schema.resume.isPublic,
isLocked: schema.resume.isLocked,
createdAt: schema.resume.createdAt,
updatedAt: schema.resume.updatedAt,
})
.from(schema.resume)
.where(
and(
eq(schema.resume.userId, input.userId),
match(input.tags.length)
.with(0, () => undefined)
.otherwise(() => arrayContains(schema.resume.tags, input.tags)),
),
)
.orderBy(
match(input.sort)
.with("lastUpdatedAt", () => desc(schema.resume.updatedAt))
.with("createdAt", () => asc(schema.resume.createdAt))
.with("name", () => asc(schema.resume.name))
.exhaustive(),
);
},
getById: async (input: { id: string; userId: string }) => {
const [resume] = await db
.select({
id: schema.resume.id,
name: schema.resume.name,
slug: schema.resume.slug,
tags: schema.resume.tags,
data: schema.resume.data,
isPublic: schema.resume.isPublic,
isLocked: schema.resume.isLocked,
hasPassword: sql<boolean>`${schema.resume.password} IS NOT NULL`,
})
.from(schema.resume)
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)));
if (!resume) throw new ORPCError("NOT_FOUND");
return resume;
},
getByIdForPrinter: async (input: { id: string }) => {
const [resume] = await db
.select({
id: schema.resume.id,
userId: schema.resume.userId,
name: schema.resume.name,
slug: schema.resume.slug,
tags: schema.resume.tags,
data: schema.resume.data,
isPublic: schema.resume.isPublic,
isLocked: schema.resume.isLocked,
})
.from(schema.resume)
.where(eq(schema.resume.id, input.id));
if (!resume) throw new ORPCError("NOT_FOUND");
return resume;
},
getBySlug: async (input: { username: string; slug: string; currentUserId?: string }) => {
const [resume] = await db
.select({
id: schema.resume.id,
name: schema.resume.name,
slug: schema.resume.slug,
tags: schema.resume.tags,
data: schema.resume.data,
isPublic: schema.resume.isPublic,
isLocked: schema.resume.isLocked,
passwordHash: schema.resume.password,
hasPassword: sql<boolean>`${schema.resume.password} IS NOT NULL`,
})
.from(schema.resume)
.innerJoin(schema.user, eq(schema.resume.userId, schema.user.id))
.where(
and(
eq(schema.resume.slug, input.slug),
eq(schema.user.username, input.username),
input.currentUserId ? eq(schema.resume.userId, input.currentUserId) : eq(schema.resume.isPublic, true),
),
);
if (!resume) throw new ORPCError("NOT_FOUND");
if (!resume.hasPassword) {
await resumeService.statistics.increment({ id: resume.id, views: true });
return {
id: resume.id,
name: resume.name,
slug: resume.slug,
tags: resume.tags,
data: resume.data,
isPublic: resume.isPublic,
isLocked: resume.isLocked,
hasPassword: false as const,
};
}
if (hasResumeAccess(resume.id, resume.passwordHash)) {
await resumeService.statistics.increment({ id: resume.id, views: true });
return {
id: resume.id,
name: resume.name,
slug: resume.slug,
tags: resume.tags,
data: resume.data,
isPublic: resume.isPublic,
isLocked: resume.isLocked,
hasPassword: true as const,
};
}
throw new ORPCError("NEED_PASSWORD", {
status: 401,
data: { username: input.username, slug: input.slug },
});
},
create: async (input: {
userId: string;
name: string;
slug: string;
tags: string[];
locale: Locale;
data?: ResumeData;
}): Promise<string> => {
const id = generateId();
input.data = input.data ?? defaultResumeData;
input.data.metadata.page.locale = input.locale;
await db.insert(schema.resume).values({
id,
name: input.name,
slug: input.slug,
tags: input.tags,
userId: input.userId,
data: input.data,
});
return id;
},
update: async (input: {
id: string;
userId: string;
name?: string;
slug?: string;
tags?: string[];
data?: ResumeData;
isPublic?: boolean;
}): Promise<void> => {
const [resume] = await db
.select({ isLocked: schema.resume.isLocked })
.from(schema.resume)
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)));
if (resume?.isLocked) throw new ORPCError("RESUME_LOCKED");
const updateData: Partial<typeof schema.resume.$inferSelect> = {
name: input.name,
slug: input.slug,
tags: input.tags,
data: input.data,
isPublic: input.isPublic,
};
await db
.update(schema.resume)
.set(updateData)
.where(
and(eq(schema.resume.id, input.id), eq(schema.resume.isLocked, false), eq(schema.resume.userId, input.userId)),
);
},
setLocked: async (input: { id: string; userId: string; isLocked: boolean }): Promise<void> => {
await db
.update(schema.resume)
.set({ isLocked: input.isLocked })
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)));
},
setPassword: async (input: { id: string; userId: string; password: string }): Promise<void> => {
const hashedPassword = await hashPassword(input.password);
await db
.update(schema.resume)
.set({ password: hashedPassword })
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)));
},
removePassword: async (input: { id: string; userId: string }): Promise<void> => {
await db
.update(schema.resume)
.set({ password: null })
.where(and(eq(schema.resume.id, input.id), eq(schema.resume.userId, input.userId)));
},
delete: async (input: { id: string; userId: string }): Promise<void> => {
const storageService = getStorageService();
const deleteResumePromise = db
.delete(schema.resume)
.where(
and(eq(schema.resume.id, input.id), eq(schema.resume.isLocked, false), eq(schema.resume.userId, input.userId)),
);
// Delete screenshots and PDFs using the new key format
const deleteScreenshotsPromise = storageService.delete(`uploads/${input.userId}/screenshots/${input.id}`);
const deletePdfsPromise = storageService.delete(`uploads/${input.userId}/pdfs/${input.id}`);
await Promise.allSettled([deleteResumePromise, deleteScreenshotsPromise, deletePdfsPromise]);
},
};
@@ -0,0 +1,93 @@
import fs from "node:fs/promises";
import { dirname, join } from "node:path";
import { count } from "drizzle-orm";
import { schema } from "@/integrations/drizzle";
import { db } from "@/integrations/drizzle/client";
const CACHE_DURATION_MS = 6 * 60 * 60 * 1000; // 6 hours
const GITHUB_API_URL = "https://api.github.com/repos/AmruthPillai/Reactive-Resume";
const LAST_KNOWN = {
users: 978_528,
resumes: 1_336_307,
stars: 34_073,
} as const;
const getCachePath = (key: string) => join(process.cwd(), "data", "statistics", `${key}.txt`);
const readCache = async (key: string): Promise<number | null> => {
try {
const filePath = getCachePath(key);
const [stats, contents] = await Promise.all([fs.stat(filePath), fs.readFile(filePath, "utf-8")]);
if (stats.mtimeMs < Date.now() - CACHE_DURATION_MS) return null;
const value = Number.parseInt(contents, 10);
return Number.isFinite(value) && value > 0 ? value : null;
} catch {
return null;
}
};
const writeCache = async (key: string, value: number) => {
try {
const filePath = getCachePath(key);
await fs.mkdir(dirname(filePath), { recursive: true });
await fs.writeFile(filePath, String(value), "utf-8");
} catch {
// Ignore errors, cache is not critical
}
};
const getCachedCount = async (
key: string,
lastKnown: number,
fetcher: () => Promise<number | null>,
): Promise<number> => {
const cached = await readCache(key);
if (cached !== null) return cached;
try {
const value = await fetcher();
if (value !== null) {
await writeCache(key, value);
return value;
}
} catch {
// Ignore errors, use last known value
}
return lastKnown;
};
const getCountFromDatabase = async (table: typeof schema.user | typeof schema.resume): Promise<number | null> => {
const [result] = await db.select({ count: count() }).from(table);
return result.count;
};
const getGitHubStars = async (): Promise<number | null> => {
const response = await fetch(GITHUB_API_URL, { headers: { Accept: "application/vnd.github+json" } });
if (!response.ok) return null;
const data = await response.json();
const stars = Number(data.stargazers_count);
return Number.isFinite(stars) && stars > 0 ? stars : null;
};
export const statisticsService = {
user: {
getCount: () => {
return getCachedCount("users", LAST_KNOWN.users, () => getCountFromDatabase(schema.user));
},
},
resume: {
getCount: () => {
return getCachedCount("resumes", LAST_KNOWN.resumes, () => getCountFromDatabase(schema.resume));
},
},
github: {
getStarCount: () => {
return getCachedCount("stars", LAST_KNOWN.stars, getGitHubStars);
},
},
};
+363
View File
@@ -0,0 +1,363 @@
import fs from "node:fs/promises";
import { dirname, extname, join } from "node:path";
import {
DeleteObjectCommand,
GetObjectCommand,
ListObjectsV2Command,
PutObjectCommand,
S3Client,
} from "@aws-sdk/client-s3";
import sharp from "sharp";
import { env } from "@/utils/env";
interface StorageWriteInput {
key: string;
data: Uint8Array;
contentType: string;
}
interface StorageReadResult {
data: Uint8Array;
size: number;
etag?: string;
lastModified?: Date;
contentType?: string;
}
interface StorageService {
list(prefix: string): Promise<string[]>;
write(input: StorageWriteInput): Promise<void>;
read(key: string): Promise<StorageReadResult | null>;
delete(key: string): Promise<boolean>;
healthcheck(): Promise<StorageHealthResult>;
}
interface StorageHealthResult {
status: "healthy" | "unhealthy";
type: "local" | "s3";
message: string;
error?: string;
}
const CONTENT_TYPE_MAP: Record<string, string> = {
".webp": "image/webp",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".svg": "image/svg+xml",
".pdf": "application/pdf",
};
const DEFAULT_CONTENT_TYPE = "application/octet-stream";
const IMAGE_MIME_TYPES = ["image/gif", "image/png", "image/jpeg", "image/webp"];
// Key builders for different upload types
function buildPictureKey(userId: string): string {
const timestamp = Date.now();
return `uploads/${userId}/pictures/${timestamp}.webp`;
}
function buildScreenshotKey(userId: string, resumeId: string): string {
const timestamp = Date.now();
return `uploads/${userId}/screenshots/${resumeId}/${timestamp}.webp`;
}
function buildPdfKey(userId: string, resumeId: string): string {
const timestamp = Date.now();
return `uploads/${userId}/pdfs/${resumeId}/${timestamp}.pdf`;
}
function buildPublicUrl(path: string): string {
return new URL(path, env.APP_URL).toString();
}
export function inferContentType(filename: string): string {
const extension = extname(filename).toLowerCase();
return CONTENT_TYPE_MAP[extension] ?? DEFAULT_CONTENT_TYPE;
}
export function isImageFile(mimeType: string): boolean {
return IMAGE_MIME_TYPES.includes(mimeType);
}
export interface ProcessedImage {
data: Uint8Array;
contentType: string;
}
export async function processImageForUpload(file: File): Promise<ProcessedImage> {
const fileBuffer = await file.arrayBuffer();
const processedBuffer = await sharp(fileBuffer)
.resize(800, 800, { fit: "inside", withoutEnlargement: true })
.webp({ preset: "picture" })
.toBuffer();
return {
data: new Uint8Array(processedBuffer),
contentType: "image/webp",
};
}
class LocalStorageService implements StorageService {
private rootDirectory: string;
constructor() {
this.rootDirectory = join(process.cwd(), "data");
}
async list(prefix: string): Promise<string[]> {
const fullPath = this.resolvePath(prefix);
try {
const files = await fs.readdir(fullPath, { recursive: true });
return files.map((file) => join(prefix, file));
} catch (error: unknown) {
// If directory doesn't exist, return empty array
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
return [];
}
throw error;
}
}
async write({ key, data }: StorageWriteInput): Promise<void> {
const fullPath = this.resolvePath(key);
await fs.mkdir(dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, data);
}
async read(key: string): Promise<StorageReadResult | null> {
const fullPath = this.resolvePath(key);
const [arrayBuffer, stats] = await Promise.all([fs.readFile(fullPath), fs.stat(fullPath)]);
return {
data: arrayBuffer,
size: stats.size,
etag: `"${stats.size}-${stats.mtime.getTime()}"`,
lastModified: stats.mtime,
contentType: inferContentType(key),
};
}
async delete(key: string): Promise<boolean> {
const fullPath = this.resolvePath(key);
// Check if the path exists and whether it's a file or folder
try {
const stats = await fs.stat(fullPath);
if (stats.isDirectory()) {
// Delete the directory and its contents recursively
await fs.rm(fullPath, { recursive: true });
return true;
} else {
await fs.unlink(fullPath);
return true;
}
} catch {
// Path does not exist
return false;
}
}
async healthcheck(): Promise<StorageHealthResult> {
try {
await fs.mkdir(this.rootDirectory, { recursive: true });
await fs.access(this.rootDirectory, fs.constants.R_OK | fs.constants.W_OK);
return {
type: "local",
status: "healthy",
message: "Local filesystem storage is accessible and has read/write permission.",
};
} catch (error: unknown) {
return {
type: "local",
status: "unhealthy",
message: "Local filesystem storage is not accessible or lacks sufficient permissions.",
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
private resolvePath(key: string): string {
const normalizedKey = key.replace(/^\/*/, "");
const segments = normalizedKey
.split(/[/\\]+/)
.filter((segment) => segment.length > 0 && segment !== "." && segment !== "..");
if (segments.length === 0) throw new Error("Invalid storage key");
return join(this.rootDirectory, ...segments);
}
}
class S3StorageService implements StorageService {
private readonly bucket: string;
private readonly client: S3Client;
constructor() {
if (!env.S3_ACCESS_KEY_ID || !env.S3_SECRET_ACCESS_KEY || !env.S3_BUCKET) {
throw new Error("S3 credentials are not set");
}
this.bucket = env.S3_BUCKET;
this.client = new S3Client({
region: env.S3_REGION,
endpoint: env.S3_ENDPOINT,
forcePathStyle: env.S3_FORCE_PATH_STYLE,
credentials: {
accessKeyId: env.S3_ACCESS_KEY_ID,
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
},
});
}
async list(prefix: string): Promise<string[]> {
const command = new ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix });
const response = await this.client.send(command);
if (!response.Contents) return [];
return response.Contents.map((object) => object.Key ?? "");
}
async write({ key, data, contentType }: StorageWriteInput): Promise<void> {
const command = new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: data,
ACL: "public-read",
ContentType: contentType,
});
await this.client.send(command);
}
async read(key: string): Promise<StorageReadResult | null> {
try {
const command = new GetObjectCommand({ Bucket: this.bucket, Key: key });
const response = await this.client.send(command);
if (!response.Body) return null;
const arrayBuffer = await response.Body.transformToByteArray();
return {
data: arrayBuffer,
size: response.ContentLength ?? 0,
etag: response.ETag,
lastModified: response.LastModified,
contentType: response.ContentType ?? inferContentType(key),
};
} catch {
return null;
}
}
async delete(keyOrPrefix: string): Promise<boolean> {
// Use list to find all matching keys (handles both single file and folder/prefix)
const keys = await this.list(keyOrPrefix);
if (keys.length === 0) return false;
// Delete all matching keys using Promise.allSettled
const deleteCommands = keys.map((k) => new DeleteObjectCommand({ Bucket: this.bucket, Key: k }));
const results = await Promise.allSettled(deleteCommands.map((c) => this.client.send(c)));
// Return true if at least one deletion succeeded
return results.some((r) => r.status === "fulfilled");
}
async healthcheck(): Promise<StorageHealthResult> {
try {
const putCommand = new PutObjectCommand({ Bucket: this.bucket, Key: "healthcheck", Body: "OK" });
await this.client.send(putCommand);
const deleteCommand = new DeleteObjectCommand({ Bucket: this.bucket, Key: "healthcheck" });
await this.client.send(deleteCommand);
return {
type: "s3",
status: "healthy",
message: "S3 storage is accessible and credentials are valid.",
};
} catch (error: unknown) {
console.error(error);
return {
type: "s3",
status: "unhealthy",
message: "Failed to connect to S3 storage or invalid credentials.",
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
}
function createStorageService(): StorageService {
if (env.S3_ACCESS_KEY_ID && env.S3_SECRET_ACCESS_KEY && env.S3_BUCKET) {
return new S3StorageService();
}
return new LocalStorageService();
}
let cachedService: StorageService | null = null;
export function getStorageService(): StorageService {
if (cachedService) return cachedService;
cachedService = createStorageService();
return cachedService;
}
// High-level upload types
type UploadType = "picture" | "screenshot" | "pdf";
export interface UploadFileInput {
userId: string;
data: Uint8Array;
contentType: string;
type: UploadType;
resumeId?: string;
}
export interface UploadFileResult {
url: string;
key: string;
}
export async function uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
const storageService = getStorageService();
let key: string;
switch (input.type) {
case "picture":
key = buildPictureKey(input.userId);
break;
case "screenshot":
if (!input.resumeId) throw new Error("resumeId is required for screenshot uploads");
key = buildScreenshotKey(input.userId, input.resumeId);
break;
case "pdf":
if (!input.resumeId) throw new Error("resumeId is required for pdf uploads");
key = buildPdfKey(input.userId, input.resumeId);
break;
}
await storageService.write({
key,
data: input.data,
contentType: input.contentType,
});
return {
key,
url: buildPublicUrl(key),
};
}
+37
View File
@@ -0,0 +1,37 @@
import { StandardRPCJsonSerializer } from "@orpc/client/standard";
import { MutationCache, QueryClient } from "@tanstack/react-query";
const serializer = new StandardRPCJsonSerializer();
export const getQueryClient = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 5 * 60 * 1000, // 5 minutes
staleTime: 60 * 1000, // 1 minute
queryKeyHashFn(queryKey) {
const [json, meta] = serializer.serialize(queryKey);
return JSON.stringify({ json, meta });
},
},
dehydrate: {
serializeData(data) {
const [json, meta] = serializer.serialize(data);
return { json, meta };
},
},
hydrate: {
deserializeData(data) {
return serializer.deserialize(data.json, data.meta);
},
},
},
mutationCache: new MutationCache({
onSettled: () => {
queryClient.invalidateQueries();
},
}),
});
return queryClient;
};