Archived
Add temporary PDF preview links via public token TTL
- Schema: add publicTokenExpiresAt to invoices table
- invoices.generatePublicToken: accepts optional ttlHours param; sets
expiry timestamp when provided
- invoices.getByPublicToken: returns 403 if token is expired
- New route GET /api/i/[token]/pdf: streams the invoice PDF publicly,
returns 410 Gone if expired; excluded from auth middleware
- MCP invoices_generate_public_token: exposes ttlHours, returns both
webUrl (/i/{token}) and pdfUrl (/api/i/{token}/pdf)
https://claude.ai/code/session_014126WHVRT8mftmqkU6dajG
This commit is contained in:
@@ -644,7 +644,7 @@ export const invoicesRouter = createTRPCRouter({
|
||||
// ── Public token (shareable link) ──────────────────────────────────────────
|
||||
|
||||
generatePublicToken: protectedProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.input(z.object({ id: z.string(), ttlHours: z.number().positive().optional() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const invoice = await ctx.db.query.invoices.findFirst({
|
||||
where: eq(invoices.id, input.id),
|
||||
@@ -653,11 +653,14 @@ export const invoicesRouter = createTRPCRouter({
|
||||
throw new TRPCError({ code: "NOT_FOUND" });
|
||||
}
|
||||
const token = crypto.randomUUID();
|
||||
const expiresAt = input.ttlHours
|
||||
? new Date(Date.now() + input.ttlHours * 3_600_000)
|
||||
: null;
|
||||
await ctx.db
|
||||
.update(invoices)
|
||||
.set({ publicToken: token })
|
||||
.set({ publicToken: token, publicTokenExpiresAt: expiresAt })
|
||||
.where(eq(invoices.id, input.id));
|
||||
return { token };
|
||||
return { token, expiresAt };
|
||||
}),
|
||||
|
||||
revokePublicToken: protectedProcedure
|
||||
@@ -684,6 +687,9 @@ export const invoicesRouter = createTRPCRouter({
|
||||
with: { client: true, business: true, items: { orderBy: (i, { asc }) => [asc(i.position)] } },
|
||||
});
|
||||
if (!invoice) throw new TRPCError({ code: "NOT_FOUND" });
|
||||
if (invoice.publicTokenExpiresAt && new Date(invoice.publicTokenExpiresAt) < new Date()) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "This link has expired" });
|
||||
}
|
||||
return invoice;
|
||||
}),
|
||||
|
||||
|
||||
@@ -367,6 +367,7 @@ export const invoices = createTable(
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
publicToken: d.varchar({ length: 255 }).unique(),
|
||||
publicTokenExpiresAt: d.timestamp(),
|
||||
lastReminderSentAt: d.timestamp(),
|
||||
createdAt: d
|
||||
.timestamp()
|
||||
|
||||
Reference in New Issue
Block a user