mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-03 18:23:47 +10:00
Resource-oriented REST aliases cover file imports (PDF text layer, JSON Resume, Reactive Resume v4/current, LinkedIn), PDF/DOCX/Markdown/JSON exports for resumes and cover letters, editor content checks, job-term matching and a public PDF readability checker. Collection endpoints accept limit/offset with X-Total-Count headers, and PATCH routes cover partial updates. Existing routes and response shapes stay unchanged. The OpenAPI spec marks public operations with `security: []`, the REST surface gets a 40 MiB body limit, and Better Auth publishes its own OpenAPI schema. Cookie sessions are rejected for cross-origin requests. DOCX export parses HTML with node-html-parser so it runs on the server, and PDF line extraction moves to @reactive-resume/import for reuse.
117 lines
4.7 KiB
TypeScript
117 lines
4.7 KiB
TypeScript
import z from "zod";
|
|
import { protectedProcedure, publicProcedure } from "../../context";
|
|
import { resumeDto } from "../../dto/resume";
|
|
import { resumeMutationRateLimit, resumePasswordRateLimit } from "../../middleware/rate-limit";
|
|
import { resumeService } from "./service";
|
|
|
|
export const sharingRouter = {
|
|
getBySlug: publicProcedure
|
|
.route({
|
|
method: "GET",
|
|
path: "/resumes/{username}/{slug}",
|
|
tags: ["Resume Sharing"],
|
|
operationId: "getResumeBySlug",
|
|
summary: "Get public resume by username and slug",
|
|
description:
|
|
"Returns a publicly shared resume identified by the owner's username and the resume's slug. A slug the resume had within the last 30 days still finds it; the response carries the current slug, so clients can redirect. If the resume is password-protected and the viewer has not yet verified the password, a 401 error with code NEED_PASSWORD is returned. No authentication required for public resumes; if authenticated as the owner, private resumes are also accessible.",
|
|
successDescription: "The public resume with its full data.",
|
|
})
|
|
.input(resumeDto.getBySlug.input)
|
|
.output(resumeDto.getBySlug.output)
|
|
.handler(({ input, context }) =>
|
|
resumeService.getBySlug({
|
|
...input,
|
|
requestHeaders: context.reqHeaders,
|
|
...(context.trustedClient ? { trustedClient: context.trustedClient } : {}),
|
|
...(context.user?.id ? { currentUserId: context.user.id } : {}),
|
|
}),
|
|
),
|
|
|
|
checkSlug: protectedProcedure
|
|
.route({
|
|
method: "GET",
|
|
path: "/resumes/{resumeId}/slug-check",
|
|
tags: ["Resume Sharing"],
|
|
operationId: "checkResumeSlug",
|
|
summary: "Check a resume address",
|
|
description:
|
|
"Checks whether a slug can be a resume's public address: lowercase letters and numbers in groups joined by single dashes, and not used by another of the user's resumes. When it can't, suggests one that works. Requires authentication.",
|
|
successDescription: "Whether the slug is available, and a suggestion when it isn't.",
|
|
})
|
|
.input(resumeDto.checkSlug.input)
|
|
.output(resumeDto.checkSlug.output)
|
|
.handler(({ input, context }) => resumeService.checkSlug({ ...input, userId: context.user.id })),
|
|
|
|
setPassword: protectedProcedure
|
|
.route({
|
|
method: "PUT",
|
|
path: "/resumes/{id}/password",
|
|
tags: ["Resume Sharing"],
|
|
operationId: "setResumePassword",
|
|
summary: "Set resume password",
|
|
description:
|
|
"Sets or updates a password on a resume. When a password is set, viewers of the public resume must enter the password before the resume data is revealed. The password must be between 6 and 64 characters. Requires authentication.",
|
|
successDescription: "The resume password was set successfully.",
|
|
})
|
|
.input(resumeDto.setPassword.input)
|
|
.use(resumeMutationRateLimit)
|
|
.output(resumeDto.setPassword.output)
|
|
.handler(({ context, input }) =>
|
|
resumeService.setPassword({
|
|
id: input.id,
|
|
userId: context.user.id,
|
|
password: input.password,
|
|
}),
|
|
),
|
|
|
|
verifyPassword: publicProcedure
|
|
.route({
|
|
method: "POST",
|
|
path: "/resumes/{username}/{slug}/password/verify",
|
|
tags: ["Resume Sharing"],
|
|
operationId: "verifyResumePassword",
|
|
summary: "Verify resume password",
|
|
description:
|
|
"Verifies a password for a password-protected public resume. On success, the viewer is granted access to view the resume data for the duration of their session. No authentication required.",
|
|
successDescription: "The password was verified successfully and access has been granted.",
|
|
})
|
|
.input(
|
|
z.object({
|
|
username: z.string().min(1).describe("The username of the resume owner."),
|
|
slug: z.string().min(1).describe("The slug of the resume."),
|
|
password: z.string().min(1).max(64).describe("The password to verify."),
|
|
}),
|
|
)
|
|
.use(resumePasswordRateLimit)
|
|
.output(z.boolean())
|
|
.handler(({ context, input }): Promise<boolean> =>
|
|
resumeService.verifyPassword({
|
|
username: input.username,
|
|
slug: input.slug,
|
|
password: input.password,
|
|
...(context.resHeaders ? { responseHeaders: context.resHeaders } : {}),
|
|
}),
|
|
),
|
|
|
|
removePassword: protectedProcedure
|
|
.route({
|
|
method: "DELETE",
|
|
path: "/resumes/{id}/password",
|
|
tags: ["Resume Sharing"],
|
|
operationId: "removeResumePassword",
|
|
summary: "Remove resume password",
|
|
description:
|
|
"Removes password protection from a resume. After removal, the resume (if public) can be viewed without entering a password. Requires authentication.",
|
|
successDescription: "The resume password was removed successfully.",
|
|
})
|
|
.input(resumeDto.removePassword.input)
|
|
.use(resumeMutationRateLimit)
|
|
.output(resumeDto.removePassword.output)
|
|
.handler(({ context, input }) =>
|
|
resumeService.removePassword({
|
|
id: input.id,
|
|
userId: context.user.id,
|
|
}),
|
|
),
|
|
};
|