Commit Graph

1813 Commits

Author SHA1 Message Date
Darko Gjorgjijoski
cc21c7d4e5 chore(release): 2.4.1
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2.4.1
2026-06-14 23:43:18 +02:00
Darko Gjorgjijoski
6f45b58933 feat(updater): disable the in-app updater in containerized installs
Consume the injected CONTAINERIZED flag: expose it on /app/version, block the update endpoints + console command, and show a 'docker compose pull' panel instead of the updater.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:43:18 +02:00
Darko Gjorgjijoski
9795bc1d6f ci(docker): release-driven tags, drop nightly cron, transitional :nightly alias
Remove scheduled nightly/alpha builds; gate :latest on LATEST_MAJOR; keep a transitional :nightly alias on stable releases. Fix the production Dockerfile so a stale host public/build cannot clobber the freshly built frontend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:43:18 +02:00
Darko Gjorgjijoski
734ac9421d fix(users): scope user view & update to the acting company
User view/update authorized only that the requester owns their active company, not that the target user belonged to it, allowing an owner of one company to read or modify users of another. Require shared company membership in UserPolicy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:43:18 +02:00
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