diff --git a/.env.example b/.env.example index 8d834b88..c8b5126d 100644 --- a/.env.example +++ b/.env.example @@ -133,6 +133,9 @@ DISCORD_WEBHOOK_URL= # Currency rate provider, currently supported: 'frankfurter' (default), 'openexchangerates' and 'nbp'. See Readme for details. CURRENCY_RATE_PROVIDER=frankfurter +# Maximum receipt upload size in MB (used by both client validation and server upload limit) +UPLOAD_MAX_FILE_SIZE_MB=10 + # Open Exchange Rates App ID OPEN_EXCHANGE_RATES_APP_ID= #********* END OF OPTIONAL ENV VARS ********* diff --git a/.oxlintrc.json b/.oxlintrc.json index 4840125f..e79edf02 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -41,7 +41,8 @@ "func-names": "off", "prefer-destructuring": "off", "max-statements": "off", - "jsx-max-depth": "off" + "jsx-max-depth": "off", + "no-nodejs-modules": "off" }, "overrides": [ { diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 6c5e7c63..9ac44821 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -26,6 +26,7 @@ At least one provider must be configured. SplitPro does not support username/pas - `DEFAULT_HOMEPAGE`: Sets the landing page route, e.g. `/home` or `/balances`. - `ENABLE_SENDING_INVITES`: Enable email invites (requires SMTP config). - `DISABLE_EMAIL_SIGNUP`: Disable email magic-link signup for new users. +- `UPLOAD_MAX_FILE_SIZE_MB`: Maximum receipt upload size in MB. Used by both client-side pre-check and server-side upload limit. Default: `10`. ## Optional variables diff --git a/src/components/AddExpense/UploadFile.tsx b/src/components/AddExpense/UploadFile.tsx index b6076d6e..c52221f9 100644 --- a/src/components/AddExpense/UploadFile.tsx +++ b/src/components/AddExpense/UploadFile.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { toast } from 'sonner'; import { useTranslation } from 'next-i18next'; -import { FILE_SIZE_LIMIT } from '~/lib/constants'; +import { env } from '~/env'; import { useAddExpenseStore } from '~/store/addStore'; import { Input } from '../ui/input'; @@ -24,8 +24,10 @@ export const UploadFile: React.FC = () => { return; } - if (file.size > FILE_SIZE_LIMIT) { - toast.error(`${t('errors.less_than')} ${FILE_SIZE_LIMIT / 1024 / 1024}MB`); + const fileSizeLimit = env.NEXT_PUBLIC_UPLOAD_MAX_FILE_SIZE_MB * 1024 * 1024; + + if (file.size > fileSizeLimit) { + toast.error(`${t('errors.less_than')} ${env.NEXT_PUBLIC_UPLOAD_MAX_FILE_SIZE_MB}MB`); return; } diff --git a/src/env.ts b/src/env.ts index f8225b11..cc83d979 100644 --- a/src/env.ts +++ b/src/env.ts @@ -70,6 +70,7 @@ export const env = createEnv({ OIDC_CLIENT_SECRET: z.string().optional(), OIDC_WELL_KNOWN_URL: z.string().optional(), OIDC_ALLOW_DANGEROUS_EMAIL_LINKING: z.boolean().optional(), + UPLOAD_MAX_FILE_SIZE_MB: z.coerce.number().int().positive().default(10), }, /** @@ -80,6 +81,7 @@ export const env = createEnv({ client: { NEXT_PUBLIC_FRANKFURTER_USED: z.boolean().default(false), NEXT_PUBLIC_IS_CLOUD_DEPLOYMENT: z.boolean().default(false), + NEXT_PUBLIC_UPLOAD_MAX_FILE_SIZE_MB: z.coerce.number().int().positive().default(10), NEXT_PUBLIC_VERSION: z.string().optional(), }, @@ -92,7 +94,7 @@ export const env = createEnv({ process.env.DATABASE_URL ?? `postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@${process.env.POSTGRES_HOST}:${process.env.POSTGRES_PORT}`, NODE_ENV: process.env.NODE_ENV, - DOCKER_OUTPUT: !!process.env.DOCKER_OUTPUT, + DOCKER_OUTPUT: Boolean(process.env.DOCKER_OUTPUT), NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, NEXTAUTH_URL: process.env.NEXTAUTH_URL, NEXTAUTH_URL_INTERNAL: process.env.NEXTAUTH_URL_INTERNAL ?? process.env.NEXTAUTH_URL, @@ -135,16 +137,22 @@ export const env = createEnv({ OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID, OIDC_CLIENT_SECRET: process.env.OIDC_CLIENT_SECRET, OIDC_WELL_KNOWN_URL: process.env.OIDC_WELL_KNOWN_URL, - OIDC_ALLOW_DANGEROUS_EMAIL_LINKING: !!process.env.OIDC_ALLOW_DANGEROUS_EMAIL_LINKING, + OIDC_ALLOW_DANGEROUS_EMAIL_LINKING: Boolean(process.env.OIDC_ALLOW_DANGEROUS_EMAIL_LINKING), NEXT_PUBLIC_FRANKFURTER_USED: process.env.CURRENCY_RATE_PROVIDER === 'frankfurter', NEXT_PUBLIC_IS_CLOUD_DEPLOYMENT: process.env.NEXTAUTH_URL?.includes('splitpro.app') ?? false, + NEXT_PUBLIC_UPLOAD_MAX_FILE_SIZE_MB: process.env.UPLOAD_MAX_FILE_SIZE_MB + ? Number(process.env.UPLOAD_MAX_FILE_SIZE_MB) + : 10, + UPLOAD_MAX_FILE_SIZE_MB: process.env.UPLOAD_MAX_FILE_SIZE_MB + ? Number(process.env.UPLOAD_MAX_FILE_SIZE_MB) + : 10, NEXT_PUBLIC_VERSION: process.env.APP_VERSION, }, /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially * useful for Docker builds. */ - skipValidation: !!process.env.SKIP_ENV_VALIDATION, + skipValidation: Boolean(process.env.SKIP_ENV_VALIDATION), /** * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and * `SOME_VAR=''` will throw an error. diff --git a/src/lib/constants.ts b/src/lib/constants.ts deleted file mode 100644 index 940333b6..00000000 --- a/src/lib/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const FILE_SIZE_LIMIT = 5 * 1024 * 1024; diff --git a/src/pages/api/upload.ts b/src/pages/api/upload.ts index 26ddf51c..ff4d6b0b 100644 --- a/src/pages/api/upload.ts +++ b/src/pages/api/upload.ts @@ -1,13 +1,14 @@ -import { randomUUID } from 'crypto'; +import { randomUUID } from 'node:crypto'; import fs from 'node:fs/promises'; -import path from 'path'; +import path from 'node:path'; import type { NextApiRequest, NextApiResponse } from 'next'; import { getServerSession } from 'next-auth/next'; import sharp from 'sharp'; import { authOptions } from '~/server/auth'; +import { env } from '~/env'; -import formidable from 'formidable'; +import { type File, formidable } from 'formidable'; import { fileExists } from '~/utils/file'; export const config = { @@ -34,10 +35,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const form = formidable({ keepExtensions: true, - maxFileSize: 10 * 1024 * 1024, + maxFileSize: env.UPLOAD_MAX_FILE_SIZE_MB * 1024 * 1024, }); - let uploadedFile: formidable.File | undefined; + let uploadedFile: File | undefined; try { const [, files] = await form.parse(req);