Appearance
Security Considerations
Authentication & Session
- Laravel Fortify provides the authentication backend (login, registration, password reset, 2FA); see
config/fortify.phpandroutes/auth.php. - Laravel Sanctum guards SPA sessions & API tokens. Ensure
SANCTUM_STATEFUL_DOMAINSis configured for cookie-based sessions in multi-domain setups.
Authorization
- Spatie Permission defines roles & permissions; keep them granular.
- Policies enforce model-level constraints; validate user context (
isShop()/isClient()). EnsureUserTypeShop/EnsureUserTypeClientgate whole route groups. Note they redirect (302) rather than returning 403.- Laravel Actions run
authorize()beforeasController()— a failure there is a 403. EnsureShopHasModule(module:<value>) is a third source of 403: the shop's plan does not include the feature, or its monthly quota is used up. It is a plan check, not a user check — see Plans & modules.auth.featuresis only shared to members. Public{shop}routes (estimate approval, CASL consent) make the URL's shop current for any signed-in user, soHandleInertiaRequests::features()also requiresisShop()andhasAccessToShop()before sharing a shop's modules. Being the current shop is not proof of membership.isShop()alone is a tenancy check, not an authorization one: every employee of a shop passes it. Anything carrying payroll or rewriting the ledger needs a policy or a permission on top — seeTimeEntryPolicy(stopping a colleague's timer re-books a billed work order) andGetSalesByTechnician(itscostcolumn divides back out to a technician's hourly rate).- A nested route parameter is only as safe as its binding. Under
->scopeBindings()a child resolves through its parent, so/work-orders/{A}/{line-of-B}is a 404 (see Multi-tenancy). A route marked->withoutScopedBindings()loses that for every parameter and must check the child/parent link itself —AssignStorageTicketandChooseWorkOrderLineVariantdo it withabort(404)inasController(). User::canManageClockFor()is the single rule behind bothShiftPunchPolicyandTimeEntryPolicy: your own clock is yours, a colleague's is a manager's, and only within the shop you both belong to.- Split read from write when two contexts share a model.
ShopPolicy::view()passes for shop staff and for a client who is a customer of that shop, because the client portal reads a garage's details through it;ShopPolicy::update()adds$user->isShop(), so being able to see a shop never implies being able to change it. A singlebelongsToShop()check covering both verbs would have handed customers write access the day the portal shipped. - A "current" tenant is a stored preference, not a proof of membership.
users.current_idis never revalidated on read, andApp\Actions\RemoveUserClientdetaches a user without clearing it, soClient::current()can still resolve a household the user has left. Every surface that resolves it without a route-bound model must guard it:abort_if($client === null, 403);then$this->authorize('view', $client);. Policies keyed on theclient_userpivot (ClientPolicy,CarPolicy,WorkOrderPolicy,ShopPolicy) are what makes that guard meaningful.
Data Validation
- Always validate incoming data via Form Requests.
- Use DTOs (Spatie Data) internally to prevent unvalidated arrays propagating.
Sensitive Configuration
Store secrets ONLY in .env:
APP_KEY,DB_PASSWORD,SENTRY_DSN,RABBITMQ_PASSWORD,COMMS_QUERY_SECRET. Never commit.env.
Logging Hygiene
- Avoid logging PII (personally identifiable information) or credentials.
- Activity logs should capture actions (who, what, when) without sensitive payloads.
Error Reporting
- Sentry captures exceptions; configure data scrubbing rules if necessary.
- Avoid exposing stack traces in production responses (exception handling configured by Laravel).
WebSockets Security
- Private & presence channels must implement authorization callbacks (in
routes/channels.php). - Reuse permission checks; do not duplicate logic.
- Model broadcasting is intentionally per-instance (
models.{tableAlias}.{id}). Do not introduce a class-wide channel: it would authorize viaviewAny(), which is not shop-scoped, and leak newly created records across tenants.
Inter-Service Security
- This application exposes no webhook endpoints. Provider webhooks and their signature validation belong to
comms-service; no route here is CSRF-exempt. - The broker is the trust boundary. RabbitMQ binds to localhost in production and carries the tenant id in every message — a consumer that ignores it would act across tenants.
COMMS_QUERY_SECRETauthenticates the read-only HTTP queries tocomms-service; it must match on both sides and never reach the client.
File & Media Handling
- Use Media Library conversions to sanitize uploads.
- Validate file types & sizes before attaching.
- Store sensitive documents on private disks (e.g., S3 with restricted ACL).
Dependencies & Updates
- Monitor Laravel, Fortify, Sanctum, Horizon, Reverb, and Sentry for security patches.
- Run
./vendor/bin/sail composer auditfor known vulnerabilities. - The Pulse dashboard carries cards for outdated (
pulse-outdated) and vulnerable (hungthai1401/vulnerable) Composer dependencies — check them periodically.
SQL & Query Safety
- Use Eloquent or parameterized queries; avoid raw concatenated SQL.
- Leverage query scopes for consistent tenancy constraints.
Rate Limiting & Throttling
- Apply throttle middleware to authentication and the
ai.endpoints. - Consider per-user or per-role limits for resource-intensive operations (file uploads, PDF generation).
Input Sanitization
- Escape dynamic output in Blade (if used) or rely on Vue binding safety.
- Strip or encode HTML in user-submitted rich text if not required (XSS mitigation).
Password & Credential Management
- Rely on Laravel's hashing configuration (bcrypt/argon2id) for user passwords.
- Enforce strong password rules in Form Requests.
Transport Security
- Serve over HTTPS; ensure correct
APP_URLconfigured. - Use secure cookies in production (
SESSION_SECURE_COOKIE=true).
Session & CSRF
- CSRF protection enabled on web routes by default.
- For APIs consumed by SPAs, ensure Sanctum token / session flow is correctly set.
Backups & Disaster Recovery (Recommendations)
- Schedule DB dumps (encrypted) stored off-site.
- Maintain media backup lifecycle (versioning & retention).
Monitoring & Alerts
- Configure Sentry alerts for high error rates.
- Consider Slack/Email notifications for Horizon job failures or queue backlog thresholds.
Least Privilege Principle
- Limit roles to necessary permissions; avoid catch-all roles.
- Restrict access to administrative dashboards (Horizon, Pulse) via gate checks.
Example Channel Authorization
php
Broadcast::channel('models.cars.{id}', function (User $user, int $id) {
return $user->isShop() && Car::query()
->whereKey($id)
->where('shop_id', $user->currentShop->id)
->exists();
});Every channel callback must re-assert the tenant, not just authentication.
Consult troubleshooting.md for resolving auth-related issues.