Harden demo access and restore clean checks
This commit is contained in:
@@ -103,6 +103,9 @@ function PublicInvoiceView({ token }: { token: string }) {
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 bg-gray-900 px-8 py-6">
|
||||
{hasLogo && (
|
||||
// Uploaded SVGs are sanitized and served by our route. next/image's
|
||||
// optimizer intentionally rejects SVG, so a native img is required.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={`/api/business-logo/${invoice.business!.id}`}
|
||||
alt=""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "server-only";
|
||||
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
import type * as S3ClientModule from "@aws-sdk/client-s3";
|
||||
|
||||
// Local dev fallback when S3_* env vars are unset. Files land in .data/receipts/.
|
||||
const LOCAL_RECEIPTS_DIR = path.join(process.cwd(), ".data", "receipts");
|
||||
@@ -17,7 +18,7 @@ export function getStorageBackend(): "s3" | "local" {
|
||||
return isS3Configured() ? "s3" : "local";
|
||||
}
|
||||
|
||||
type S3Module = typeof import("@aws-sdk/client-s3");
|
||||
type S3Module = typeof S3ClientModule;
|
||||
|
||||
let s3ModulePromise: Promise<S3Module> | null = null;
|
||||
let s3Client: InstanceType<S3Module["S3Client"]> | null = null;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
/** Stored on entries clocked in before empty descriptions were allowed. */
|
||||
export const LEGACY_DEFAULT_CLOCK_DESCRIPTION = "Professional services";
|
||||
|
||||
export function normalizeOptionalId(value?: string | null): string | null {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed == null || trimmed === "" ? null : trimmed;
|
||||
}
|
||||
|
||||
export function resolveEffectiveHourlyRate(
|
||||
enteredRate: number,
|
||||
client?: { defaultHourlyRate?: number | null } | null,
|
||||
|
||||
@@ -20,9 +20,15 @@ import {
|
||||
RECEIPT_MAX_BYTES,
|
||||
} from "~/lib/object-storage";
|
||||
import { parseReceiptText } from "~/lib/receipt-parse";
|
||||
import type { db } from "~/server/db";
|
||||
|
||||
export { EXPENSE_CATEGORIES };
|
||||
|
||||
type ExpenseContext = {
|
||||
db: typeof db;
|
||||
session: { user: { id: string } };
|
||||
};
|
||||
|
||||
const createExpenseSchema = z.object({
|
||||
date: z.date(),
|
||||
description: z.string().min(1, "Description is required"),
|
||||
@@ -43,7 +49,7 @@ const updateExpenseSchema = createExpenseSchema.partial().extend({
|
||||
});
|
||||
|
||||
async function verifyClientAccess(
|
||||
ctx: { db: typeof import("~/server/db").db; session: { user: { id: string } } },
|
||||
ctx: ExpenseContext,
|
||||
clientId: string,
|
||||
) {
|
||||
const client = await ctx.db.query.clients.findFirst({
|
||||
@@ -62,7 +68,7 @@ async function verifyClientAccess(
|
||||
}
|
||||
|
||||
async function verifyInvoiceAccess(
|
||||
ctx: { db: typeof import("~/server/db").db; session: { user: { id: string } } },
|
||||
ctx: ExpenseContext,
|
||||
invoiceId: string,
|
||||
) {
|
||||
const invoice = await ctx.db.query.invoices.findFirst({
|
||||
@@ -81,7 +87,7 @@ async function verifyInvoiceAccess(
|
||||
}
|
||||
|
||||
async function resolveExpenseBusinessId(
|
||||
ctx: { db: typeof import("~/server/db").db; session: { user: { id: string } } },
|
||||
ctx: ExpenseContext,
|
||||
businessId: string | null,
|
||||
invoice?: { businessId: string | null } | null,
|
||||
) {
|
||||
@@ -98,7 +104,7 @@ async function resolveExpenseBusinessId(
|
||||
}
|
||||
|
||||
async function getOwnedExpense(
|
||||
ctx: { db: typeof import("~/server/db").db; session: { user: { id: string } } },
|
||||
ctx: ExpenseContext,
|
||||
expenseId: string,
|
||||
) {
|
||||
const expense = await ctx.db.query.expenses.findFirst({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TRPCError } from "@trpc/server";
|
||||
import type { db } from "~/server/db";
|
||||
import {
|
||||
computeTrackedHours,
|
||||
normalizeOptionalId,
|
||||
resolveBillingDescription,
|
||||
type ClockOutOutcome,
|
||||
} from "~/lib/time-clock";
|
||||
@@ -242,7 +243,7 @@ export const timeEntriesRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const clientId = input.clientId?.trim() || null;
|
||||
const clientId = normalizeOptionalId(input.clientId);
|
||||
let clientRecord: { defaultHourlyRate: number | null } | null = null;
|
||||
if (clientId) {
|
||||
const found = await ctx.db.query.clients.findFirst({
|
||||
@@ -514,7 +515,7 @@ export const timeEntriesRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(createSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const clientId = input.clientId?.trim() || null;
|
||||
const clientId = normalizeOptionalId(input.clientId);
|
||||
if (clientId) {
|
||||
const client = await ctx.db.query.clients.findFirst({
|
||||
where: and(eq(clients.id, clientId), eq(clients.createdById, ctx.session.user.id)),
|
||||
|
||||
Reference in New Issue
Block a user