Skip to content

Releases: liberusoftware/ecommerce-laravel

1.0.3

Choose a tag to compare

@tdrabikdev tdrabikdev released this 15 Jul 14:29
82959f0

Registration works again — and the /app panel is shut. Those were one bug.

1245 tests pass (7 added). Verified live end-to-end.

Versioning note. Continues the v1 line. The release before v1.0.0 was v13.0.0, which tracked the Laravel major, and Packagist still resolves that as highest — so composer create-project gets v13.0.0 unless you pin 1.0.3. The v1 tag now points here.

The coupling

CreateNewUser attached every registrant to Team::first() — the merchant's own team, seeded by DefaultTeamSeeder and owned by the admin — then assigned a staff role. The method is named assignOrCreateTeam and its docblock promises "or create a personal team"; neither branch ever existed.

Meanwhile canAccessPanel ended return true; // TODO: Check panel and role, so a user with zero roles and zero teams was waved into /app — Products, Orders, Invoices, Customers.

It only failed to bite because registration threw first: RolesSeeder stopped creating staff in cf711cb without touching the caller, and on an unseeded database Team::first() returned null and fatally errored a line earlier. Fixing registration alone would have opened the door.

What changed

  • Registration produces a shopper: a user, no team, no role. Team membership gates /app, and teams are granted through TeamPolicy::create (which needs the create_store permission) — handing one out at signup bypassed that gate entirely.
  • canAccessPanel asks the right question per panel: adminsuper_admin, app → team membership. It also now reads $this rather than auth()->user(), which authorized the session user instead of the one passed in.
  • Login and register default to the storefront, using intended() so a shopper interrupted mid-checkout returns there. Shoppers lose nothing: /orders, /wishlist, /invoices, /payment_methods are unaffected.

The test that hid it

FortifyActionsTest::makeTeam() created the team and the staff role — with a comment saying so. That is exactly why CI stayed green while every real registration threw. It's gone; those tests still pass.

Known issues

  • ArticleResource / CollectionResource have no policy and strictAuthorization is off, so Filament allows: a team member can CRUD their own tenant's articles and collections (price included) without a permission check. No longer reachable by a stranger — that takes a team. Needs policies and seeded permissions together, because the Shield set has no article_*/collection_* entries at all.
  • SVG stored XSS is latent — product upload accepts image/svg+xml onto a public disk. Unreachable today (no public/storage symlink); live the moment storage:link runs.
  • /user/profile breaks if social login is enabled// use HasConnectedAccounts; is commented out in User, so connectedAccounts is null and Socialstream calls ->map() on it.
  • Password::default() is min 8, no complexity, no breach check.
  • The seeder sets inventory_count = 0 on every product, so the demo storefront ships sold-out.
  • RoleBasedRedirect is dead code inherited from a property-management app (its six targets all 404), and config('permission.teams') is false so roles are global despite code reading as team-scoped.

Full changelog: v1.0.2...v1.0.3

1.0.2

Choose a tag to compare

@tdrabikdev tdrabikdev released this 15 Jul 14:00
049f328

A contact form that actually sends, and a security gate that can finally report.

1238 tests pass (11 added). Verified end-to-end against a real SMTP inbox, not mocks.

Versioning note. Continues the v1 line. The release before v1.0.0 was v13.0.0, which tracked the Laravel major, and Composer still orders 13.0.0 above this tag — so composer create-project resolves v13.0.0 unless you pin 1.0.2 explicitly. The v1 tag now points here.

The contact form now sends

It previously sent nothing, anywhere. The page was Preline's SaaS template pasted verbatim: the <form> had no action, no method, no @csrf, and Route::view meant no handler existed to receive it — submitting just reloaded the page. A second, orphaned form component posted to /contact/send, which 404s and which nothing rendered.

Now: routes, a controller, validation, and a Mailable to the merchant's configured site_email with Reply-To set to the visitor, so replying is one click. From stays the app's own sender — sending as the visitor would fail SPF/DKIM and land in spam.

Anti-spam: a honeypot plus throttle:5,1. The honeypot is checked before validation and answers exactly as a success would — validating it would hand a bot the name of the field that caught it. It's sr-only + aria-hidden + tabindex="-1"; the tab order was verified in a browser to skip it entirely.

Fixed alongside: {{ env('APP_NAME') }} in a view (returns null under config:cache, so it rendered blank in production); field names left as the template's hs-firstname-contacts-1; placeholder-as-label, which vanishes the moment you type; and the four "Knowledgebase / Developer APIs / Contact sales" blocks — SaaS copy on a shop, all four linking to #. They're replaced by the contact details the merchant has actually configured, each shown only when set.

The security audit job works

Dependency Security Audit had failed on every run since at least 2026-07-14 and had never reported on a single dependency. It ran composer audit with no vendor/ tree and no --locked, so it died on Composer's own advice every time.

It now audits the lock file directly — the versions that actually deploy — and reports zero advisories. Which it could never have told anyone before.

A permanently-red security gate is worse than no gate: it trains everyone to scroll past the one check whose entire job is to shout. It sat red through the v1.0.1 security release.

Known issues, carried forward

  • Public registration is broken, and that bug is the only thing masking an access-control hole. CreateNewUser assigns a staff role no seeder creates, so signup rolls back. Behind it, every registrant would join the store's primary team, canAccessPanel returns true for /app, and Article/ProductCollection have no policies (Filament allows by default). Must be fixed together.
  • SVG stored XSS is latent — product upload accepts image/svg+xml onto a public disk. Unreachable today (no public/storage symlink); live the moment storage:link runs.
  • /user/profile renders only because social login defaults off; enabling a provider re-breaks it (// use HasConnectedAccounts; is commented out in User).
  • Password::default() is min 8, no complexity, no breach check.
  • The seeder sets inventory_count = 0 on every product, so the demo storefront ships sold-out.
  • The Dependency Security Audit job covers Composer only; the project also ships npm dependencies. npm audit --omit=dev reports 0 today.

Full changelog: v1.0.1...v1.0.2

1.0.1

Choose a tag to compare

@tdrabikdev tdrabikdev released this 15 Jul 13:21
8a106bf

Security release. Fixes the findings from an OWASP audit of the codebase.

1227 tests pass (25 added — every fix has a regression test, because these hid in plain sight). composer audit --locked and npm audit --omit=dev both report zero.

Versioning note. This continues the v1 line started at v1.0.0. The release before that was v13.0.0, whose tag tracked the Laravel major, and Composer still orders 13.0.0 above this tag — so composer create-project resolves v13.0.0 unless you pin 1.0.1 explicitly. The v1 tag now points here.

🔴 Critical

Any registered customer could ship goods at the merchant's expense. The dropshipping API group had auth:sanctum and no role check — the only API controller without one. DropshippingService::placeOrder makes a live call to the supplier with the merchant's API key and a caller-supplied payload, and registration is open. Register → mint a token → POST /api/dropshipping/place-order with any items and address → real goods ship, billed to the merchant. No order record, no payment, no limit. track-order was additionally an unscoped IDOR over any supplier reference. Now staff-only on all four endpoints.

🟠 High

  • Plaintext passwords written to logs. Failed registration stripped password but not password_confirmation — the same value. The common trigger is a duplicate email, where the password is perfectly valid. The QueryException path leaked the bcrypt hash twice, because formatMessage() interpolates bindings into the message.
  • Webhook signing secrets returned on every response. No $hidden, so listing endpoints returned every whsec_ secret — the keys needed to forge deliveries that consumers validate as authentic. The code claimed "returned once"; it wasn't.
  • No security headers at all — no CSP, HSTS, X-Frame-Options, nosniff or Referrer-Policy. The admin panels were framable. CSP ships as Report-Only so it can't break Filament/Livewire.
  • /checkout/process was public and unthrottled, reaching payment capture with a caller-supplied card token: unlimited card testing.
  • The installer's key check failed open. INSTALLER_ENABLED=true without a key exposed shell execution, .env rewrite and arbitrary-role user creation to anyone who found the URL. Now fails closed.

🟡 Medium

Guest API rate limiting had collapsed into a single shared bucket — no proxies were trusted, so every client looked like the ingress and one attacker could throttle everyone. Wildcard CORS on api/* is now env-driven. The session cookie now gets Secure outside local/testing. Authentication failures and lockouts are now logged (allowlisting the email out of the credentials array, which holds the password). SSRF targets rejected on webhook URLs.

Known issues, not fixed here

  • Public registration is broken, and that bug is the only thing masking an access-control hole. CreateNewUser assigns a staff role no seeder creates, so signup rolls back. Behind it, every registrant would join the store's primary team, canAccessPanel returns true for /app, and Article/ProductCollection have no policies (Filament allows by default). These must be fixed together — repairing registration alone opens the store's tenant to every signup.
  • SVG stored XSS is latent. Product image upload accepts image/svg+xml onto a public disk. Currently unreachable (no public/storage symlink) but live the moment storage:link runs.
  • Password::default() is min 8 with no complexity or breach check.
  • The Dependency Security Audit workflow is red and has been since before this work — it runs composer audit with no composer install and no --locked. One flag fixes it.
  • /user/profile renders only because social login now defaults off; enabling a provider re-breaks it (// use HasConnectedAccounts; is commented out in User).

Full changelog: v1.0.0...v1.0.1

1.0.0

Choose a tag to compare

@tdrabikdev tdrabikdev released this 15 Jul 12:01
ebe9361

Storefront design system — and the eight pages that turned out to be hard-500.

1202 tests pass, 0 skipped. Every surface screenshotted and read at desktop and mobile.

Versioning note. This release deliberately restarts the version line. The previous release was v13.0.0, whose tag tracked the Laravel major. Composer orders 13.0.0 above 1.0.0, so a ^13 constraint will not resolve this tag, and v13.0.0 may continue to show as "Latest". If you install by version, pin 1.0.0 explicitly.

Fixed: eight pages were 500ing

<x-guest-layout> and <x-app-layout> were referenced throughout but never existed:

  • Two-factor challenge. Fortify has twoFactorAuthentication() enabled, so anyone who turned 2FA on was locked out permanently.
  • Password reset from the emailed link — the escape hatch from exactly that lockout.
  • Email verification, confirm password.
  • Profile, teams, API tokens (the missing app-layout).

The only page that can enable 2FA is /user/profile, which was also crashing — the lockout was unreachable purely because the door to it was broken too.

Security

  • Removed a "Demo Credentials" panel that rendered unconditionally in production, advertising admin@example.com and staff@example.com beside the word "password". The seeder randomises that password, so it was useless to honest visitors and a free username list for everyone else.
  • Social login is now opt-in (SOCIALSTREAM_PROVIDERS, default empty). Nine providers were enabled with credentials configured for none, so every button threw DriverMissingConfigurationException on click.

Removed fabricated content

A default theme ships as somebody else's real store, which makes this material:

  • Three hardcoded testimonials ("John Doe", "Sarah Smith", five stars each) — every merchant published fake reviews.
  • A hardcoded "Summer Sale — 50% off" pointing at an image that cannot exist.
  • Four invented categories, replaced with real ProductCategory data.
  • Four simulated navbar features — including an addToWishlist that made no request and reported "Added to wishlist!" anyway.
  • product.blade.php, orphaned and carrying a fake gallery and fake "Related Product 1..N".

Design system

Workshop Moss in app.css's @theme. No blue (the first-order ecommerce reflex). No second green — moss doubles as the affirmative signal. Inter + IBM Plex Mono, self-hosted; mono reserved for prices, quantities and 2FA codes. PaletteContrastTest holds all 25 tokens inside the sRGB gamut with every pair clearing WCAG 2.2 AA.

products/show was Bootstrap markup inside a Tailwind app, so stock status rendered as plain black text. Rebuilt.

Known issues

  • /user/profile renders only because social login now defaults off. With providers enabled it still 500s: app/Models/User.php:35 has // use HasConnectedAccounts; commented out. config/socialstream.php warns at the point of enabling.
  • 105 blue literals remain in contact/checkout/categories.
  • The seeder sets inventory_count = 0 on every product, so the demo storefront ships sold-out.
  • The Dependency Security Audit workflow is red and has been since before this work: it runs composer audit with no composer install and no --locked.

Full changelog: v13.0.0...v1.0.0

v13.0.0

Choose a tag to compare

@curtisdelicata curtisdelicata released this 30 May 19:39

Initial Laravel 13 release.