Commit Graph

1809 Commits

Author SHA1 Message Date
Darko Gjorgjijoski
d2465c8a4e ci: bump ramsey/composer-install v2 -> 4.0.0 (node24 internal cache) (#677)
Follow-up to #676: composer-install@v2 is composite but internally calls
actions/cache@v3 (Node 20), the last Node-20 deprecation on 2.x CI. 4.0.0
uses actions/cache v5.0.3 (Node 24) and keeps the composer-options input.
2026-06-12 13:15:51 +02:00
Darko Gjorgjijoski
0cdf3063ed ci: bump GitHub Actions to Node 24 versions (#676)
GitHub removes Node 20 runners on 2026-09-16; bump every Node-20 action
so v2 CI keeps working through the 2.x security-support window. checkout
v4->v6, setup-node v4->v6, paths-filter v3->v4, cancel-workflow-action
0.12.1->0.13.1, pnpm/action-setup v4->v6, docker/{setup-buildx v3->v4,
login v3->v4, metadata v5->v6, build-push v5->v7}. setup-php@v2,
ramsey/composer-install@v2 and svenstaro/upload-release-action@v2 are
already on node24.
2026-06-12 12:26:15 +02:00
Darko Gjorgjijoski
e3ab12ad77 docs: add Supported Versions policy (2.x feature freeze, security until 2027-09-01) (#675) 2.4.0 2026-06-12 10:14:56 +02:00
Darko Gjorgjijoski
fdb9a9d860 chore: bump version to 2.4.0 (#667) 2026-06-12 09:48:27 +02:00
Darko Gjorgjijoski
9c5b8776c6 fix(deps): bump vulnerable dependencies to patched versions (#674)
Resolves the open Dependabot alerts on 2.x plus a laravel/framework
advisory surfaced by composer audit:

- vite 8.0.3 -> 8.0.5 (npm) — keeps rolldown 1.0.0-rc.12 so the build
  stays correct; vite 8.0.15+ pulls the broken rolldown 1.0.3
- laravel/framework -> 13.15.0 (CVE-2026-48019, CRLF injection in the
  default email validation rule)
- symfony/{mime,http-kernel,mailer,routing,yaml,polyfill-intl-idn} and
  guzzlehttp/psr7 -> patched releases

composer audit clean; frontend build verified (no dangling chunk refs).
2026-06-12 09:47:43 +02:00
Darko Gjorgjijoski
2f641ace0e build: migrate frontend tooling to pnpm + pin vite 8.0.3 (#666)
* build: migrate frontend tooling from yarn to pnpm

The Dockerfiles ran `yarn && yarn build`, which broke on node:24 (yarn no
longer on PATH; the corepack yarn shim made `npm i -g yarn` fail EEXIST),
while CI + Makefile used npm and only a yarn.lock was committed — an
inconsistent yarn/npm split. Standardize on pnpm, pinned via the
packageManager field + corepack.

- package.json: packageManager pnpm@11.6.0.
- pnpm-workspace.yaml: nodeLinker: hoisted (flat node_modules, npm/yarn-like, so
  directly-imported transitive deps like flatpickr resolve) + allow vue-demi's
  postinstall (it selects the Vue 3 entry). pnpm 11 reads these here, not .npmrc.
- Generate pnpm-lock.yaml (imported from yarn.lock); delete yarn.lock.
- Dockerfiles (dev/nginx/production): node:24 + `corepack enable && pnpm install --frozen-lockfile && pnpm build`.
- CI (check.yaml, docker.yaml): pnpm/action-setup + setup-node cache:pnpm; pnpm install --frozen-lockfile / pnpm build.
- Makefile, composer.json dev script, CLAUDE.md: npm/yarn -> pnpm.

pnpm build verified on a clean install (1425 modules, hoisted node_modules).

* fix(build): pin vite to 8.0.3 to fix rolldown chunk regression

vite 8.0.16 (pulled in by #653) bundles rolldown 1.0.3, which emits a
lazy chunk referencing an undefined Vue runtime-init function
(init_runtime_dom_esm_bundler), breaking the SPA at runtime. The build
succeeds so CI never caught it. Pin vite to 8.0.3 (the version 2.3.3
shipped, rolldown 1.0.0) which produces a correct bundle.
2026-06-12 09:31:08 +02:00
Darko Gjorgjijoski
107a951126 fix(security): recompute document totals server-side (GHSA-8c69) (#665)
Invoice/estimate/recurring-invoice creation and update accepted total,
sub_total, tax and due_amount straight from the request with no server-side
recalculation, so a client could persist financial totals that don't match the
line items (and, on invoice update, corrupt the due-amount/paid-amount logic
which keyed off the client total).

- Adds App\Support\DocumentTotals which recomputes item totals (round(price *
  quantity) minus per-item discount) and document totals (sub_total, tax with
  per-item vs document and tax_included handling, total, due_amount), mirroring
  the front-end calculation. Only price/quantity/discounts/tax-line amounts are
  trusted.
- getInvoicePayload/getEstimatePayload/getRecurringInvoicePayload override the
  client totals with the recomputed values; createItems recomputes each stored
  item total; Invoice::updateInvoice keys its due-amount/paid-amount logic off
  the recomputed total instead of the request total.

Adds DocumentTotals unit tests + a feature test proving a tampered invoice
total is ignored. Existing create/update tests no longer assert the (now
server-authoritative) derived totals.
2026-06-12 09:30:44 +02:00
Darko Gjorgjijoski
d615cfb4ff fix(security): block SSRF via the Gotenberg host setting (GHSA-mfxg) (#664)
gotenberg_host was validated only with Laravel's 'url' rule, which permits
loopback/private/link-local hosts (e.g. http://127.0.0.1, http://10.0.0.1,
the cloud metadata endpoint http://169.254.169.254). When a PDF renders, the
server POSTs the document HTML to that host — an SSRF primitive.

- Adds App\Rules\SafeRemoteUrl: requires http(s) and rejects any host that
  resolves to a loopback/private/link-local/CGNAT/reserved address (IPv4 and
  IPv6), including literal-IP hosts.
- Wires it into PDFConfigurationRequest for gotenberg_host.
- Adds a defensive re-check in GotenbergPDFDriver before the outbound call to
  cover hosts set via env/seed/stale config or DNS rebinding (TOCTOU).

Adds unit tests for the rule + validator integration.
2026-06-12 09:18:57 +02:00
Darko Gjorgjijoski
e92b08ef6a fix(security): block ORDER BY SQL injection via orderByField (GHSA-cp8p) (#663)
The orderByField/orderBy query params were passed straight into Eloquent's
orderBy() in every model's scopeWhereOrder (and Invoice::scopeApplyFilters),
allowing arbitrary SQL in the ORDER BY clause (boolean-based blind injection).

Adds App\Support\SafeOrderBy::apply() which only accepts a plain, optionally
table-qualified column identifier as the sort target (rejecting expressions,
sub-selects, etc.) and clamps the direction to asc/desc. Routed all 10 model
sort sinks through it. Table-qualified columns stay valid, so joined/aliased
sorts (e.g. estimates by customers.name) are unaffected.

Adds unit tests covering injection rejection, plain + aliased columns, and
direction clamping.
2026-06-12 09:18:29 +02:00
Darko Gjorgjijoski
5839d8385d fix(security): harden public EmailLog token endpoints (GHSA-73q7) (#662)
Customer PDF controllers resolved the target document by raw mailable_id,
ignoring mailable_type, and skipped the expiry check on the JSON endpoints.

- Resolve via the $emailLog->mailable morph relation and assert the expected
  type (abort 404) so a token issued for one document type can't disclose
  another whose numeric id collides.
- Enforce isExpired() (abort 403) on every public path, including the JSON
  getInvoice/getEstimate/getPayment endpoints.
- Harden EmailLog::isExpired() to treat a null/unresolvable mailable as
  expired instead of throwing.

Adds tests for cross-type 404, JSON-path expiry 403, and the valid path.
2026-06-12 09:18:16 +02:00
Darko Gjorgjijoski
e432e4e62f fix(security): enforce company scope on notes, estimate-convert, and user bulk-delete (#661)
- Notes IDOR (GHSA-85wc): NotePolicy::viewNotes/manageNotes now receive the
  Note and require hasCompany($note->company_id); NotesController passes the
  bound model to authorize() on show/update/destroy.
- Estimate->Invoice IDOR (GHSA-j2vg): ConvertEstimateController authorizes
  'view' on the source estimate before creating the invoice.
- User bulk-delete (GHSA-wxrv): UsersController scopes candidate ids via
  User::whereCompany() before deletion so cross-company accounts are protected.

Adds feature tests for cross-company 403s plus same-company happy paths.
2026-06-12 09:17:52 +02:00
Darko Gjorgjijoski
c1cadb7ee0 fix(updater): manifest-based stale-file cleanup + cache clearing for v2→v3 (#659)
The v2 self-updater only overlays new files (copyFiles) and never removes
files a release deleted; the one removal path (deleted_files) is not even
sent by the web UI. A major upgrade (v2 → v3) removes thousands of files,
so overlay-only leaves a broken hybrid, and stale bootstrap/cache config +
package-discovery survive and break the new boot.

Backport v3's manifest allow-list approach into this final v2 release:

- Updater::cleanStaleFiles(?string $basePath): delete every file under the
  install not listed in the release's manifest.json, except the configured
  update_protected_paths. No manifest present → safe no-op (v2→v2 updates).
- Updater::clearCompiledCaches(): wipe bootstrap/cache/*.php and compiled
  views so the freshly copied release re-reads config and re-runs package
  discovery. Called at the end of copyFiles() — the last point that runs as
  the currently-installed code before the new release boots, and necessary
  because bootstrap/cache is itself a protected path.
- DeleteFilesController + UpdateCommand: when manifest.json is present, run
  cleanStaleFiles(); otherwise fall back to the legacy deleted_files list.
  No route or frontend change — both already call the delete step between
  copy and migrate.
- config/invoiceshelf.php: add update_protected_paths (.env, storage,
  vendor, node_modules, Modules, public/storage, .git, bootstrap/cache,
  manifest.json).

The v3 release zip already ships manifest.json (built by its make dist), so
a v2 instance running this updater cleans itself up correctly on upgrade.

Tested: tests/Unit/UpdaterTest.php covers stale removal, protected-path and
manifest preservation, empty-dir pruning, no-manifest no-op, and invalid
manifest. Full suite green.
2026-06-12 09:10:49 +02:00
Darko Gjorgjijoski
c9d623a0dd Revert "Export system"
This reverts commit a79c4ec5ee.
2026-06-11 08:36:05 +02:00
Rihards Simanovičs
ed5103e54d chore: resign from project (#651) 2026-06-05 00:48:47 +02:00
Darko Gjorgjijoski
8c412fa2cf fix(security): bump axios, vite, postcss, follow-redirects to patched versions (#653)
Clears all 28 open Dependabot alerts on 2.x (13 high, 14 medium, 1 low),
which collapse to four frontend packages:

- axios 1.14.0 -> 1.16.0  (the bulk; exact-pinned, so Dependabot couldn't auto-PR)
- vite 8.0.3 -> 8.0.16    (>= 8.0.5 patched)
- follow-redirects 1.15.11 -> 1.16.0
- postcss 8.5.8 -> 8.5.15 (>= 8.5.10 patched)

Supersedes Dependabot PRs #629 (vite) and #630 (follow-redirects).
2026-06-05 00:41:32 +02:00
Darko Gjorgjijoski
0c47869c51 Swap master with versioned branch for nightlies
2.x is still nightly
2026-06-05 00:22:55 +02:00
mchev
8e6e7f9981 Merge pull request #649 from mchev/exports
Add CSV exports for customers, items, invoices, estimates, and expenses
2026-06-04 17:28:45 +02:00
mchev
a79c4ec5ee Export system 2026-06-04 17:20:00 +02:00
mchev
83caff13cd Merge pull request #615 from InvoiceShelf/translations
New Crowdin updates
2026-04-08 09:29:39 +02:00
mchev
d2736b1c46 Merge pull request #616 from mchev/taxdecimal
Support 3-decimal tax percentages (e.g. 6.625%)
2026-04-08 09:28:44 +02:00
mchev
cdc504f518 Merge pull request #617 from mchev/duplicate_expense
Add duplicate expense action
2026-04-08 09:28:22 +02:00
Darko Gjorgjijoski
2c840b2d97 Bump version from 2.3.2 to 2.3.3 2.3.3 2026-04-07 21:25:20 +02:00
Darko Gjorgjijoski
34db4b7897 Sanitize PDF address fields against SSRF, not just notes
Closes the residual surface from the three published SSRF advisories (GHSA-pc5v-8xwc-v9xq, GHSA-38hf-fq8x-q49r, GHSA-q9wx-ggwq-mcgh / CVE-2026-34365 to 34367). The original fix in 07757e74 only sanitized the Notes field via Invoice/Estimate/Payment::getNotes(), but the same blade templates also render company/billing/shipping address fields with {!! !!} (Blade unescaped output). Those address strings are produced by getCompanyAddress(), getCustomerBillingAddress(), getCustomerShippingAddress() which feed into GeneratesPdfTrait::getFormattedString() — and that method does not call PdfHtmlSanitizer.

Customer-controlled fields (name, street, phone, custom field values) are substituted into address templates via getFieldsArray() without HTML-escaping. A malicious customer name like "Acme <img src='http://attacker/probe'>" therefore reaches Dompdf as raw HTML through the address path, exactly the same CWE-918 SSRF pattern the advisories describe — only blocked today by the secondary defense of dompdf's enable_remote=false. If a self-hoster sets DOMPDF_ENABLE_REMOTE=true for legitimate remote logos, the address surface immediately re-opens.

Move the PdfHtmlSanitizer::sanitize() call into the chokepoint at GeneratesPdfTrait::getFormattedString(), so all four sinks — notes plus the three address fields, on all three models — get the same treatment via a single call site. The explicit wrapper in each model's getNotes() becomes redundant and is removed (along with the now-unused App\Support\PdfHtmlSanitizer imports). Verified getFormattedString() is only called from PDF code paths (no email body callers, which use strtr() directly) so there is no risk of stripping useful HTML from a non-PDF context.

Extends tests/Unit/PdfHtmlSanitizerTest.php with three new cases covering the address-template scenario, iframe/link tag stripping, and on* event handler removal. All 8 tests pass via vendor/bin/pest tests/Unit/PdfHtmlSanitizerTest.php.
2026-04-07 20:39:19 +02:00
mchev
beb2a43ed3 Duplicate expense 2026-04-07 19:00:07 +02:00
mchev
7a25a15877 Increasing tax decimal 2026-04-07 18:35:30 +02:00
Darko Gjorgjijoski
8eee4bc4f5 New translations en.json (Serbian (Latin)) 2026-04-07 14:51:02 +02:00
Darko Gjorgjijoski
c34cfeea11 New translations en.json (Swahili) 2026-04-07 14:51:01 +02:00
Darko Gjorgjijoski
f37edbcc92 New translations en.json (Malay) 2026-04-07 14:50:59 +02:00
Darko Gjorgjijoski
ba581313df New translations en.json (Hindi) 2026-04-07 14:50:58 +02:00
Darko Gjorgjijoski
1c1a180dd5 New translations en.json (Latvian) 2026-04-07 14:50:57 +02:00
Darko Gjorgjijoski
8e815fd887 New translations en.json (Estonian) 2026-04-07 14:50:56 +02:00
Darko Gjorgjijoski
df612bc773 New translations en.json (Croatian) 2026-04-07 14:50:55 +02:00
Darko Gjorgjijoski
10b2cf5af1 New translations en.json (Thai) 2026-04-07 14:50:53 +02:00
Darko Gjorgjijoski
f140256efd New translations en.json (Bengali) 2026-04-07 14:50:52 +02:00
Darko Gjorgjijoski
1ed8e5b2d0 New translations en.json (Persian) 2026-04-07 14:50:50 +02:00
Darko Gjorgjijoski
fcb7c96bca New translations en.json (Indonesian) 2026-04-07 14:50:49 +02:00
Darko Gjorgjijoski
d587e3fd00 New translations en.json (Portuguese, Brazilian) 2026-04-07 14:50:48 +02:00
Darko Gjorgjijoski
1b26d47539 New translations en.json (Vietnamese) 2026-04-07 14:50:47 +02:00
Darko Gjorgjijoski
340522da19 New translations en.json (Urdu (Pakistan)) 2026-04-07 14:50:45 +02:00
Darko Gjorgjijoski
3ecfcede3e New translations en.json (Chinese Traditional) 2026-04-07 14:50:44 +02:00
Darko Gjorgjijoski
30f76c044a New translations en.json (Chinese Simplified) 2026-04-07 14:50:43 +02:00
Darko Gjorgjijoski
854a8bb50d New translations en.json (Ukrainian) 2026-04-07 14:50:41 +02:00
Darko Gjorgjijoski
1be3132dc4 New translations en.json (Turkish) 2026-04-07 14:50:40 +02:00
Darko Gjorgjijoski
fefd856cfb New translations en.json (Swedish) 2026-04-07 14:50:39 +02:00
Darko Gjorgjijoski
e5fe62e4de New translations en.json (Albanian) 2026-04-07 14:50:38 +02:00
Darko Gjorgjijoski
129d63c5b1 New translations en.json (Slovenian) 2026-04-07 14:50:37 +02:00
Darko Gjorgjijoski
65e9182272 New translations en.json (Slovak) 2026-04-07 14:50:35 +02:00
Darko Gjorgjijoski
3abcba2752 New translations en.json (Russian) 2026-04-07 14:50:34 +02:00
Darko Gjorgjijoski
6c1e51d126 New translations en.json (Portuguese) 2026-04-07 14:50:33 +02:00
Darko Gjorgjijoski
c1b8ba448d New translations en.json (Polish) 2026-04-07 14:50:31 +02:00